huskies: merge 543_story_resume_failed_coder_agents_with_resume_instead_of_starting_fresh_sessions
This commit is contained in:
@@ -262,7 +262,7 @@ impl AgentPool {
|
|||||||
"[auto-assign] Assigning '{agent_name}' to '{story_id}' in {stage_dir}/"
|
"[auto-assign] Assigning '{agent_name}' to '{story_id}' in {stage_dir}/"
|
||||||
);
|
);
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.start_agent(project_root, story_id, Some(&agent_name), None)
|
.start_agent(project_root, story_id, Some(&agent_name), None, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
slog!(
|
slog!(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use super::super::{AgentPool, StoryAgent};
|
|||||||
impl AgentPool {
|
impl AgentPool {
|
||||||
/// Pipeline advancement: after an agent completes, move the story to
|
/// Pipeline advancement: after an agent completes, move the story to
|
||||||
/// the next pipeline stage and start the appropriate agent.
|
/// the next pipeline stage and start the appropriate agent.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) async fn run_pipeline_advance(
|
pub(super) async fn run_pipeline_advance(
|
||||||
&self,
|
&self,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
@@ -25,6 +26,7 @@ impl AgentPool {
|
|||||||
project_root: Option<PathBuf>,
|
project_root: Option<PathBuf>,
|
||||||
worktree_path: Option<PathBuf>,
|
worktree_path: Option<PathBuf>,
|
||||||
merge_failure_reported: bool,
|
merge_failure_reported: bool,
|
||||||
|
previous_session_id: Option<String>,
|
||||||
) {
|
) {
|
||||||
let project_root = match project_root {
|
let project_root = match project_root {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
@@ -81,7 +83,7 @@ impl AgentPool {
|
|||||||
if let Err(e) = crate::agents::lifecycle::move_story_to_qa(&project_root, story_id) {
|
if let Err(e) = crate::agents::lifecycle::move_story_to_qa(&project_root, story_id) {
|
||||||
slog_error!("[pipeline] Failed to move '{story_id}' to 3_qa/: {e}");
|
slog_error!("[pipeline] Failed to move '{story_id}' to 3_qa/: {e}");
|
||||||
} else if let Err(e) = self
|
} else if let Err(e) = self
|
||||||
.start_agent(&project_root, story_id, Some("qa"), None)
|
.start_agent(&project_root, story_id, Some("qa"), None, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
slog_error!("[pipeline] Failed to start qa agent for '{story_id}': {e}");
|
slog_error!("[pipeline] Failed to start qa agent for '{story_id}': {e}");
|
||||||
@@ -118,7 +120,13 @@ impl AgentPool {
|
|||||||
completion.gate_output
|
completion.gate_output
|
||||||
);
|
);
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.start_agent(&project_root, story_id, Some(agent_name), Some(&context))
|
.start_agent(
|
||||||
|
&project_root,
|
||||||
|
story_id,
|
||||||
|
Some(agent_name),
|
||||||
|
Some(&context),
|
||||||
|
previous_session_id,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
slog_error!(
|
slog_error!(
|
||||||
@@ -202,7 +210,7 @@ impl AgentPool {
|
|||||||
coverage_output
|
coverage_output
|
||||||
);
|
);
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.start_agent(&project_root, story_id, Some("qa"), Some(&context))
|
.start_agent(&project_root, story_id, Some("qa"), Some(&context), None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
slog_error!("[pipeline] Failed to restart qa for '{story_id}': {e}");
|
slog_error!("[pipeline] Failed to restart qa for '{story_id}': {e}");
|
||||||
@@ -223,7 +231,7 @@ impl AgentPool {
|
|||||||
completion.gate_output
|
completion.gate_output
|
||||||
);
|
);
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.start_agent(&project_root, story_id, Some("qa"), Some(&context))
|
.start_agent(&project_root, story_id, Some("qa"), Some(&context), None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
slog_error!("[pipeline] Failed to restart qa for '{story_id}': {e}");
|
slog_error!("[pipeline] Failed to restart qa for '{story_id}': {e}");
|
||||||
@@ -322,6 +330,7 @@ impl AgentPool {
|
|||||||
story_id,
|
story_id,
|
||||||
Some("mergemaster"),
|
Some("mergemaster"),
|
||||||
Some(&context),
|
Some(&context),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -369,7 +378,7 @@ impl AgentPool {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.start_agent(project_root, story_id, Some("mergemaster"), None)
|
.start_agent(project_root, story_id, Some("mergemaster"), None, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
slog_error!("[pipeline] Failed to start mergemaster for '{story_id}': {e}");
|
slog_error!("[pipeline] Failed to start mergemaster for '{story_id}': {e}");
|
||||||
@@ -392,6 +401,7 @@ pub(super) fn spawn_pipeline_advance(
|
|||||||
worktree_path: Option<PathBuf>,
|
worktree_path: Option<PathBuf>,
|
||||||
watcher_tx: broadcast::Sender<WatcherEvent>,
|
watcher_tx: broadcast::Sender<WatcherEvent>,
|
||||||
merge_failure_reported: bool,
|
merge_failure_reported: bool,
|
||||||
|
previous_session_id: Option<String>,
|
||||||
) {
|
) {
|
||||||
let sid = story_id.to_string();
|
let sid = story_id.to_string();
|
||||||
let aname = agent_name.to_string();
|
let aname = agent_name.to_string();
|
||||||
@@ -410,6 +420,7 @@ pub(super) fn spawn_pipeline_advance(
|
|||||||
project_root,
|
project_root,
|
||||||
worktree_path,
|
worktree_path,
|
||||||
merge_failure_reported,
|
merge_failure_reported,
|
||||||
|
previous_session_id,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
@@ -524,6 +535,7 @@ mod tests {
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -565,6 +577,7 @@ mod tests {
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -606,6 +619,7 @@ mod tests {
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -639,6 +653,7 @@ mod tests {
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -706,6 +721,7 @@ stage = "qa"
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -777,6 +793,7 @@ stage = "qa"
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -883,6 +900,7 @@ stage = "qa"
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -950,6 +968,7 @@ stage = "qa"
|
|||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ impl AgentPool {
|
|||||||
project_root_for_advance,
|
project_root_for_advance,
|
||||||
wt_path_for_advance,
|
wt_path_for_advance,
|
||||||
merge_failure_reported_for_advance,
|
merge_failure_reported_for_advance,
|
||||||
|
session_id_for_advance,
|
||||||
) = {
|
) = {
|
||||||
let mut agents = self.agents.lock().map_err(|e| e.to_string())?;
|
let mut agents = self.agents.lock().map_err(|e| e.to_string())?;
|
||||||
let agent = agents.get_mut(&key).ok_or_else(|| {
|
let agent = agents.get_mut(&key).ok_or_else(|| {
|
||||||
@@ -94,8 +95,9 @@ impl AgentPool {
|
|||||||
let pr = agent.project_root.clone();
|
let pr = agent.project_root.clone();
|
||||||
let wt = agent.worktree_info.as_ref().map(|w| w.path.clone());
|
let wt = agent.worktree_info.as_ref().map(|w| w.path.clone());
|
||||||
let mfr = agent.merge_failure_reported;
|
let mfr = agent.merge_failure_reported;
|
||||||
|
let sid_advance = agent.session_id.clone();
|
||||||
agents.remove(&key);
|
agents.remove(&key);
|
||||||
(tx, sid, pr, wt, mfr)
|
(tx, sid, pr, wt, mfr, sid_advance)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emit Done so wait_for_agent unblocks.
|
// Emit Done so wait_for_agent unblocks.
|
||||||
@@ -128,6 +130,7 @@ impl AgentPool {
|
|||||||
project_root_for_advance,
|
project_root_for_advance,
|
||||||
wt_path_for_advance,
|
wt_path_for_advance,
|
||||||
merge_failure_reported_for_advance,
|
merge_failure_reported_for_advance,
|
||||||
|
session_id_for_advance,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
@@ -277,6 +280,8 @@ pub(in crate::agents::pool) async fn run_server_owned_completion(
|
|||||||
lock.remove(&key);
|
lock.remove(&key);
|
||||||
(tx, pr, wt, mfr)
|
(tx, pr, wt, mfr)
|
||||||
};
|
};
|
||||||
|
// The completed session's ID is used to resume if gates fail.
|
||||||
|
let previous_session_id = session_id.clone();
|
||||||
|
|
||||||
// Emit Done so wait_for_agent unblocks.
|
// Emit Done so wait_for_agent unblocks.
|
||||||
let _ = tx.send(AgentEvent::Done {
|
let _ = tx.send(AgentEvent::Done {
|
||||||
@@ -299,6 +304,7 @@ pub(in crate::agents::pool) async fn run_server_owned_completion(
|
|||||||
wt_path_for_advance,
|
wt_path_for_advance,
|
||||||
watcher_tx,
|
watcher_tx,
|
||||||
merge_failure_reported_for_advance,
|
merge_failure_reported_for_advance,
|
||||||
|
previous_session_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,22 @@ impl AgentPool {
|
|||||||
/// agent (story 190). If all coders are busy the call fails with an error
|
/// agent (story 190). If all coders are busy the call fails with an error
|
||||||
/// indicating the story will be picked up when one becomes available.
|
/// indicating the story will be picked up when one becomes available.
|
||||||
///
|
///
|
||||||
/// If `resume_context` is provided, it is appended to the rendered prompt
|
/// If `resume_context` is provided and `session_id_to_resume` is `None`,
|
||||||
/// so the agent can pick up from a previous failed attempt.
|
/// the context is appended to the rendered prompt so the agent can pick up
|
||||||
|
/// from a previous failed attempt.
|
||||||
|
///
|
||||||
|
/// If `session_id_to_resume` is provided, the agent is launched with
|
||||||
|
/// `--resume <session_id>` instead of `-p <full_prompt>`. Only
|
||||||
|
/// `resume_context` (if any) is sent as the new message. This lets
|
||||||
|
/// the agent re-enter the previous conversation without re-reading
|
||||||
|
/// CLAUDE.md and README, satisfying story 543.
|
||||||
pub async fn start_agent(
|
pub async fn start_agent(
|
||||||
&self,
|
&self,
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
agent_name: Option<&str>,
|
agent_name: Option<&str>,
|
||||||
resume_context: Option<&str>,
|
resume_context: Option<&str>,
|
||||||
|
session_id_to_resume: Option<String>,
|
||||||
) -> Result<AgentInfo, String> {
|
) -> Result<AgentInfo, String> {
|
||||||
let config = ProjectConfig::load(project_root)?;
|
let config = ProjectConfig::load(project_root)?;
|
||||||
|
|
||||||
@@ -310,6 +318,7 @@ impl AgentPool {
|
|||||||
let project_root_clone = project_root.to_path_buf();
|
let project_root_clone = project_root.to_path_buf();
|
||||||
let config_clone = config.clone();
|
let config_clone = config.clone();
|
||||||
let resume_context_owned = resume_context.map(str::to_string);
|
let resume_context_owned = resume_context.map(str::to_string);
|
||||||
|
let session_id_to_resume_owned = session_id_to_resume;
|
||||||
let sid = story_id.to_string();
|
let sid = story_id.to_string();
|
||||||
let aname = resolved_name.clone();
|
let aname = resolved_name.clone();
|
||||||
let tx_clone = tx.clone();
|
let tx_clone = tx.clone();
|
||||||
@@ -397,10 +406,21 @@ impl AgentPool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Append resume context if this is a restart with failure information.
|
// Build the effective prompt and determine resume session.
|
||||||
|
//
|
||||||
|
// When resuming a previous session, discard the full rendered prompt
|
||||||
|
// (which would re-read CLAUDE.md and README) and send only the gate
|
||||||
|
// failure context as a new message. On a fresh start, append the
|
||||||
|
// failure context to the original prompt as before.
|
||||||
|
let effective_prompt = match &session_id_to_resume_owned {
|
||||||
|
Some(_) => resume_context_owned.unwrap_or_default(),
|
||||||
|
None => {
|
||||||
if let Some(ctx) = resume_context_owned {
|
if let Some(ctx) = resume_context_owned {
|
||||||
prompt.push_str(&ctx);
|
prompt.push_str(&ctx);
|
||||||
}
|
}
|
||||||
|
prompt
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Step 3: transition to Running now that the worktree is ready.
|
// Step 3: transition to Running now that the worktree is ready.
|
||||||
{
|
{
|
||||||
@@ -431,10 +451,11 @@ impl AgentPool {
|
|||||||
agent_name: aname.clone(),
|
agent_name: aname.clone(),
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
prompt,
|
prompt: effective_prompt,
|
||||||
cwd: wt_path_str,
|
cwd: wt_path_str,
|
||||||
inactivity_timeout_secs,
|
inactivity_timeout_secs,
|
||||||
mcp_port: port_for_task,
|
mcp_port: port_for_task,
|
||||||
|
session_id_to_resume: session_id_to_resume_owned.clone(),
|
||||||
};
|
};
|
||||||
runtime
|
runtime
|
||||||
.start(ctx, tx_clone.clone(), log_clone.clone(), log_writer_clone)
|
.start(ctx, tx_clone.clone(), log_clone.clone(), log_writer_clone)
|
||||||
@@ -447,10 +468,11 @@ impl AgentPool {
|
|||||||
agent_name: aname.clone(),
|
agent_name: aname.clone(),
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
prompt,
|
prompt: effective_prompt,
|
||||||
cwd: wt_path_str,
|
cwd: wt_path_str,
|
||||||
inactivity_timeout_secs,
|
inactivity_timeout_secs,
|
||||||
mcp_port: port_for_task,
|
mcp_port: port_for_task,
|
||||||
|
session_id_to_resume: session_id_to_resume_owned.clone(),
|
||||||
};
|
};
|
||||||
runtime
|
runtime
|
||||||
.start(ctx, tx_clone.clone(), log_clone.clone(), log_writer_clone)
|
.start(ctx, tx_clone.clone(), log_clone.clone(), log_writer_clone)
|
||||||
@@ -463,10 +485,11 @@ impl AgentPool {
|
|||||||
agent_name: aname.clone(),
|
agent_name: aname.clone(),
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
prompt,
|
prompt: effective_prompt,
|
||||||
cwd: wt_path_str,
|
cwd: wt_path_str,
|
||||||
inactivity_timeout_secs,
|
inactivity_timeout_secs,
|
||||||
mcp_port: port_for_task,
|
mcp_port: port_for_task,
|
||||||
|
session_id_to_resume: session_id_to_resume_owned,
|
||||||
};
|
};
|
||||||
runtime
|
runtime
|
||||||
.start(ctx, tx_clone.clone(), log_clone.clone(), log_writer_clone)
|
.start(ctx, tx_clone.clone(), log_clone.clone(), log_writer_clone)
|
||||||
@@ -646,7 +669,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("other-story", "coder-1", AgentStatus::Running);
|
pool.inject_test_agent("other-story", "coder-1", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(tmp.path(), "42_my_story", None, None)
|
.start_agent(tmp.path(), "42_my_story", None, None, None)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
@@ -688,7 +711,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running);
|
pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running);
|
||||||
pool.inject_test_agent("story-2", "coder-2", AgentStatus::Pending);
|
pool.inject_test_agent("story-2", "coder-2", AgentStatus::Pending);
|
||||||
|
|
||||||
let result = pool.start_agent(tmp.path(), "story-3", None, None).await;
|
let result = pool.start_agent(tmp.path(), "story-3", None, None, None).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -720,7 +743,7 @@ stage = "coder"
|
|||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running);
|
pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool.start_agent(tmp.path(), "story-3", None, None).await;
|
let result = pool.start_agent(tmp.path(), "story-3", None, None, None).await;
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
@@ -758,7 +781,7 @@ stage = "coder"
|
|||||||
|
|
||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
|
|
||||||
let result = pool.start_agent(tmp.path(), "story-5", None, None).await;
|
let result = pool.start_agent(tmp.path(), "story-5", None, None, None).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -793,7 +816,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running);
|
pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(tmp.path(), "story-2", Some("coder-1"), None)
|
.start_agent(tmp.path(), "story-2", Some("coder-1"), None, None)
|
||||||
.await;
|
.await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
@@ -819,7 +842,7 @@ stage = "coder"
|
|||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
pool.inject_test_agent("story-a", "qa", AgentStatus::Running);
|
pool.inject_test_agent("story-a", "qa", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool.start_agent(root, "story-b", Some("qa"), None).await;
|
let result = pool.start_agent(root, "story-b", Some("qa"), None, None).await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
result.is_err(),
|
result.is_err(),
|
||||||
@@ -846,7 +869,7 @@ stage = "coder"
|
|||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
pool.inject_test_agent("story-a", "qa", AgentStatus::Completed);
|
pool.inject_test_agent("story-a", "qa", AgentStatus::Completed);
|
||||||
|
|
||||||
let result = pool.start_agent(root, "story-b", Some("qa"), None).await;
|
let result = pool.start_agent(root, "story-b", Some("qa"), None, None).await;
|
||||||
|
|
||||||
if let Err(ref e) = result {
|
if let Err(ref e) = result {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -880,7 +903,7 @@ stage = "coder"
|
|||||||
let pool = AgentPool::new_test(3099);
|
let pool = AgentPool::new_test(3099);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "50_story_test", Some("coder-1"), None)
|
.start_agent(root, "50_story_test", Some("coder-1"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -938,7 +961,7 @@ stage = "coder"
|
|||||||
let pool = AgentPool::new_test(3099);
|
let pool = AgentPool::new_test(3099);
|
||||||
pool.inject_test_agent("story-x", "qa", AgentStatus::Running);
|
pool.inject_test_agent("story-x", "qa", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool.start_agent(root, "story-y", Some("qa"), None).await;
|
let result = pool.start_agent(root, "story-y", Some("qa"), None, None).await;
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
@@ -969,7 +992,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("86_story_foo", "coder-1", AgentStatus::Pending);
|
pool.inject_test_agent("86_story_foo", "coder-1", AgentStatus::Pending);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "130_story_bar", Some("coder-1"), None)
|
.start_agent(root, "130_story_bar", Some("coder-1"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(result.is_err(), "second start_agent must be rejected");
|
assert!(result.is_err(), "second start_agent must be rejected");
|
||||||
@@ -1012,7 +1035,7 @@ stage = "coder"
|
|||||||
let root1 = root.clone();
|
let root1 = root.clone();
|
||||||
let t1 = tokio::spawn(async move {
|
let t1 = tokio::spawn(async move {
|
||||||
pool1
|
pool1
|
||||||
.start_agent(&root1, "86_story_foo", Some("coder-1"), None)
|
.start_agent(&root1, "86_story_foo", Some("coder-1"), None, None)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1020,7 +1043,7 @@ stage = "coder"
|
|||||||
let root2 = root.clone();
|
let root2 = root.clone();
|
||||||
let t2 = tokio::spawn(async move {
|
let t2 = tokio::spawn(async move {
|
||||||
pool2
|
pool2
|
||||||
.start_agent(&root2, "130_story_bar", Some("coder-1"), None)
|
.start_agent(&root2, "130_story_bar", Some("coder-1"), None, None)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1065,7 +1088,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("42_story_foo", "coder-1", AgentStatus::Running);
|
pool.inject_test_agent("42_story_foo", "coder-1", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "42_story_foo", Some("coder-2"), None)
|
.start_agent(root, "42_story_foo", Some("coder-2"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1103,7 +1126,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("55_story_bar", "qa-1", AgentStatus::Running);
|
pool.inject_test_agent("55_story_bar", "qa-1", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "55_story_bar", Some("qa-2"), None)
|
.start_agent(root, "55_story_bar", Some("qa-2"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(result.is_err(), "second qa on same story must be rejected");
|
assert!(result.is_err(), "second qa on same story must be rejected");
|
||||||
@@ -1141,7 +1164,7 @@ stage = "coder"
|
|||||||
let root1 = root.clone();
|
let root1 = root.clone();
|
||||||
let t1 = tokio::spawn(async move {
|
let t1 = tokio::spawn(async move {
|
||||||
pool1
|
pool1
|
||||||
.start_agent(&root1, "42_story_foo", Some("coder-1"), None)
|
.start_agent(&root1, "42_story_foo", Some("coder-1"), None, None)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1149,7 +1172,7 @@ stage = "coder"
|
|||||||
let root2 = root.clone();
|
let root2 = root.clone();
|
||||||
let t2 = tokio::spawn(async move {
|
let t2 = tokio::spawn(async move {
|
||||||
pool2
|
pool2
|
||||||
.start_agent(&root2, "42_story_foo", Some("coder-2"), None)
|
.start_agent(&root2, "42_story_foo", Some("coder-2"), None, None)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1193,7 +1216,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("42_story_foo", "coder-1", AgentStatus::Running);
|
pool.inject_test_agent("42_story_foo", "coder-1", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "99_story_baz", Some("coder-2"), None)
|
.start_agent(root, "99_story_baz", Some("coder-2"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(ref e) = result {
|
if let Err(ref e) = result {
|
||||||
@@ -1231,7 +1254,7 @@ stage = "coder"
|
|||||||
|
|
||||||
let pool = AgentPool::new_test(3099);
|
let pool = AgentPool::new_test(3099);
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "310_story_foo", Some("mergemaster"), None)
|
.start_agent(root, "310_story_foo", Some("mergemaster"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1269,7 +1292,7 @@ stage = "coder"
|
|||||||
|
|
||||||
let pool = AgentPool::new_test(3099);
|
let pool = AgentPool::new_test(3099);
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "8842_story_qa_guard", Some("coder-1"), None)
|
.start_agent(root, "8842_story_qa_guard", Some("coder-1"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1307,7 +1330,7 @@ stage = "coder"
|
|||||||
|
|
||||||
let pool = AgentPool::new_test(3099);
|
let pool = AgentPool::new_test(3099);
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "55_story_baz", Some("qa"), None)
|
.start_agent(root, "55_story_baz", Some("qa"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1343,7 +1366,7 @@ stage = "coder"
|
|||||||
|
|
||||||
let pool = AgentPool::new_test(3099);
|
let pool = AgentPool::new_test(3099);
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "77_story_sup", Some("supervisor"), None)
|
.start_agent(root, "77_story_sup", Some("supervisor"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
@@ -1379,7 +1402,7 @@ stage = "coder"
|
|||||||
|
|
||||||
let pool = AgentPool::new_test(3099);
|
let pool = AgentPool::new_test(3099);
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "88_story_ok", Some("mergemaster"), None)
|
.start_agent(root, "88_story_ok", Some("mergemaster"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
@@ -1435,7 +1458,7 @@ stage = "coder"
|
|||||||
|
|
||||||
let pool = AgentPool::new_test(3098);
|
let pool = AgentPool::new_test(3098);
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(root, "502_story_split_brain", Some("mergemaster"), None)
|
.start_agent(root, "502_story_split_brain", Some("mergemaster"), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Stage check must not reject mergemaster.
|
// Stage check must not reject mergemaster.
|
||||||
@@ -1494,7 +1517,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("other-story", "coder-sonnet", AgentStatus::Running);
|
pool.inject_test_agent("other-story", "coder-sonnet", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(tmp.path(), "368_story_test", None, None)
|
.start_agent(tmp.path(), "368_story_test", None, None, None)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
@@ -1557,7 +1580,7 @@ stage = "coder"
|
|||||||
pool.inject_test_agent("other-story", "coder-opus", AgentStatus::Running);
|
pool.inject_test_agent("other-story", "coder-opus", AgentStatus::Running);
|
||||||
|
|
||||||
let result = pool
|
let result = pool
|
||||||
.start_agent(tmp.path(), "368_story_test", None, None)
|
.start_agent(tmp.path(), "368_story_test", None, None, None)
|
||||||
.await;
|
.await;
|
||||||
assert!(result.is_err(), "expected error when preferred agent is busy");
|
assert!(result.is_err(), "expected error when preferred agent is busy");
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub(in crate::agents) async fn run_agent_pty_streaming(
|
|||||||
inactivity_timeout_secs: u64,
|
inactivity_timeout_secs: u64,
|
||||||
child_killers: Arc<Mutex<HashMap<String, Box<dyn ChildKiller + Send + Sync>>>>,
|
child_killers: Arc<Mutex<HashMap<String, Box<dyn ChildKiller + Send + Sync>>>>,
|
||||||
watcher_tx: broadcast::Sender<WatcherEvent>,
|
watcher_tx: broadcast::Sender<WatcherEvent>,
|
||||||
|
session_id_to_resume: Option<&str>,
|
||||||
) -> Result<PtyResult, String> {
|
) -> Result<PtyResult, String> {
|
||||||
let sid = story_id.to_string();
|
let sid = story_id.to_string();
|
||||||
let aname = agent_name.to_string();
|
let aname = agent_name.to_string();
|
||||||
@@ -58,6 +59,7 @@ pub(in crate::agents) async fn run_agent_pty_streaming(
|
|||||||
let cwd = cwd.to_string();
|
let cwd = cwd.to_string();
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
let event_log = event_log.clone();
|
let event_log = event_log.clone();
|
||||||
|
let resume_sid = session_id_to_resume.map(str::to_string);
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
run_agent_pty_blocking(
|
run_agent_pty_blocking(
|
||||||
@@ -73,6 +75,7 @@ pub(in crate::agents) async fn run_agent_pty_streaming(
|
|||||||
inactivity_timeout_secs,
|
inactivity_timeout_secs,
|
||||||
&child_killers,
|
&child_killers,
|
||||||
&watcher_tx,
|
&watcher_tx,
|
||||||
|
resume_sid.as_deref(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -166,6 +169,7 @@ fn run_agent_pty_blocking(
|
|||||||
inactivity_timeout_secs: u64,
|
inactivity_timeout_secs: u64,
|
||||||
child_killers: &Arc<Mutex<HashMap<String, Box<dyn ChildKiller + Send + Sync>>>>,
|
child_killers: &Arc<Mutex<HashMap<String, Box<dyn ChildKiller + Send + Sync>>>>,
|
||||||
watcher_tx: &broadcast::Sender<WatcherEvent>,
|
watcher_tx: &broadcast::Sender<WatcherEvent>,
|
||||||
|
session_id_to_resume: Option<&str>,
|
||||||
) -> Result<PtyResult, String> {
|
) -> Result<PtyResult, String> {
|
||||||
let pty_system = native_pty_system();
|
let pty_system = native_pty_system();
|
||||||
|
|
||||||
@@ -180,9 +184,21 @@ fn run_agent_pty_blocking(
|
|||||||
|
|
||||||
let mut cmd = CommandBuilder::new(command);
|
let mut cmd = CommandBuilder::new(command);
|
||||||
|
|
||||||
// -p <prompt> must come first
|
// Launch mode: resume an existing session or start fresh.
|
||||||
|
if let Some(sid) = session_id_to_resume {
|
||||||
|
// Resume: --resume <session_id> restores previous conversation context.
|
||||||
|
// Only the failure context (prompt) is sent as a new message via -p.
|
||||||
|
cmd.arg("--resume");
|
||||||
|
cmd.arg(sid);
|
||||||
|
if !prompt.is_empty() {
|
||||||
cmd.arg("-p");
|
cmd.arg("-p");
|
||||||
cmd.arg(prompt);
|
cmd.arg(prompt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fresh session: deliver the full rendered prompt via -p.
|
||||||
|
cmd.arg("-p");
|
||||||
|
cmd.arg(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
// Add configured args (e.g., --directory /path/to/worktree, --model, etc.)
|
// Add configured args (e.g., --directory /path/to/worktree, --model, etc.)
|
||||||
for arg in args {
|
for arg in args {
|
||||||
@@ -541,6 +557,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
child_killers,
|
child_killers,
|
||||||
watcher_tx,
|
watcher_tx,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -594,6 +611,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
child_killers,
|
child_killers,
|
||||||
watcher_tx,
|
watcher_tx,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -655,6 +673,7 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
child_killers,
|
child_killers,
|
||||||
watcher_tx,
|
watcher_tx,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let after = chrono::Utc::now();
|
let after = chrono::Utc::now();
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ impl AgentRuntime for ClaudeCodeRuntime {
|
|||||||
ctx.inactivity_timeout_secs,
|
ctx.inactivity_timeout_secs,
|
||||||
Arc::clone(&self.child_killers),
|
Arc::clone(&self.child_killers),
|
||||||
self.watcher_tx.clone(),
|
self.watcher_tx.clone(),
|
||||||
|
ctx.session_id_to_resume.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -707,6 +707,7 @@ mod tests {
|
|||||||
cwd: "/tmp/wt".to_string(),
|
cwd: "/tmp/wt".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let instruction = build_system_instruction(&ctx);
|
let instruction = build_system_instruction(&ctx);
|
||||||
@@ -724,6 +725,7 @@ mod tests {
|
|||||||
cwd: "/tmp/wt".to_string(),
|
cwd: "/tmp/wt".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let instruction = build_system_instruction(&ctx);
|
let instruction = build_system_instruction(&ctx);
|
||||||
@@ -800,6 +802,7 @@ mod tests {
|
|||||||
cwd: "/tmp".to_string(),
|
cwd: "/tmp".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// The model extraction logic is inside start(), but we test the
|
// The model extraction logic is inside start(), but we test the
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ pub struct RuntimeContext {
|
|||||||
/// Port of the huskies MCP server, used by API-based runtimes (Gemini, OpenAI)
|
/// Port of the huskies MCP server, used by API-based runtimes (Gemini, OpenAI)
|
||||||
/// to call back for tool execution.
|
/// to call back for tool execution.
|
||||||
pub mcp_port: u16,
|
pub mcp_port: u16,
|
||||||
|
/// When set, resume a previous Claude Code session instead of starting fresh.
|
||||||
|
///
|
||||||
|
/// The CLI is invoked as `claude --resume <session_id> [-p <prompt>]` rather
|
||||||
|
/// than `claude -p <full_prompt>`. The agent re-enters the previous
|
||||||
|
/// conversation and receives the `prompt` (if non-empty) as a new message.
|
||||||
|
pub session_id_to_resume: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result returned by a runtime after the agent session completes.
|
/// Result returned by a runtime after the agent session completes.
|
||||||
@@ -93,6 +99,7 @@ mod tests {
|
|||||||
cwd: "/tmp/wt".to_string(),
|
cwd: "/tmp/wt".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
assert_eq!(ctx.story_id, "42_story_foo");
|
assert_eq!(ctx.story_id, "42_story_foo");
|
||||||
assert_eq!(ctx.agent_name, "coder-1");
|
assert_eq!(ctx.agent_name, "coder-1");
|
||||||
|
|||||||
@@ -618,6 +618,7 @@ mod tests {
|
|||||||
cwd: "/tmp/wt".to_string(),
|
cwd: "/tmp/wt".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(build_system_text(&ctx), "Custom system prompt");
|
assert_eq!(build_system_text(&ctx), "Custom system prompt");
|
||||||
@@ -634,6 +635,7 @@ mod tests {
|
|||||||
cwd: "/tmp/wt".to_string(),
|
cwd: "/tmp/wt".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = build_system_text(&ctx);
|
let text = build_system_text(&ctx);
|
||||||
@@ -683,6 +685,7 @@ mod tests {
|
|||||||
cwd: "/tmp".to_string(),
|
cwd: "/tmp".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
assert!(ctx.command.starts_with("gpt"));
|
assert!(ctx.command.starts_with("gpt"));
|
||||||
}
|
}
|
||||||
@@ -698,6 +701,7 @@ mod tests {
|
|||||||
cwd: "/tmp".to_string(),
|
cwd: "/tmp".to_string(),
|
||||||
inactivity_timeout_secs: 300,
|
inactivity_timeout_secs: 300,
|
||||||
mcp_port: 3001,
|
mcp_port: 3001,
|
||||||
|
session_id_to_resume: None,
|
||||||
};
|
};
|
||||||
assert!(ctx.command.starts_with("o"));
|
assert!(ctx.command.starts_with("o"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ pub(crate) async fn tick_once(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match agents
|
match agents
|
||||||
.start_agent(project_root, &entry.story_id, None, None)
|
.start_agent(project_root, &entry.story_id, None, None, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ pub async fn handle_assign(
|
|||||||
);
|
);
|
||||||
|
|
||||||
match agents
|
match agents
|
||||||
.start_agent(project_root, &story_id, Some(&agent_name), None)
|
.start_agent(project_root, &story_id, Some(&agent_name), None, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ pub async fn handle_start(
|
|||||||
);
|
);
|
||||||
|
|
||||||
match agents
|
match agents
|
||||||
.start_agent(project_root, &story_id, resolved_agent.as_deref(), None)
|
.start_agent(project_root, &story_id, resolved_agent.as_deref(), None, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ impl AgentsApi {
|
|||||||
&payload.0.story_id,
|
&payload.0.story_id,
|
||||||
payload.0.agent_name.as_deref(),
|
payload.0.agent_name.as_deref(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(bad_request)?;
|
.map_err(bad_request)?;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub(super) async fn tool_start_agent(args: &Value, ctx: &AppContext) -> Result<S
|
|||||||
let project_root = ctx.agents.get_project_root(&ctx.state)?;
|
let project_root = ctx.agents.get_project_root(&ctx.state)?;
|
||||||
let info = ctx
|
let info = ctx
|
||||||
.agents
|
.agents
|
||||||
.start_agent(&project_root, story_id, agent_name, None)
|
.start_agent(&project_root, story_id, agent_name, None, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Snapshot coverage baseline from the most recent coverage report (best-effort).
|
// Snapshot coverage baseline from the most recent coverage report (best-effort).
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ pub(super) async fn tool_move_story_to_merge(args: &Value, ctx: &AppContext) ->
|
|||||||
// Start the mergemaster agent on the story worktree
|
// Start the mergemaster agent on the story worktree
|
||||||
let info = ctx
|
let info = ctx
|
||||||
.agents
|
.agents
|
||||||
.start_agent(&project_root, story_id, Some(agent_name), None)
|
.start_agent(&project_root, story_id, Some(agent_name), None, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
serde_json::to_string_pretty(&json!({
|
serde_json::to_string_pretty(&json!({
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub(super) async fn tool_request_qa(args: &Value, ctx: &AppContext) -> Result<St
|
|||||||
// Start the QA agent on the story worktree
|
// Start the QA agent on the story worktree
|
||||||
let info = ctx
|
let info = ctx
|
||||||
.agents
|
.agents
|
||||||
.start_agent(&project_root, story_id, Some(agent_name), None)
|
.start_agent(&project_root, story_id, Some(agent_name), None, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
serde_json::to_string_pretty(&json!({
|
serde_json::to_string_pretty(&json!({
|
||||||
@@ -60,7 +60,7 @@ pub(super) async fn tool_approve_qa(args: &Value, ctx: &AppContext) -> Result<St
|
|||||||
// Start the mergemaster agent
|
// Start the mergemaster agent
|
||||||
let info = ctx
|
let info = ctx
|
||||||
.agents
|
.agents
|
||||||
.start_agent(&project_root, story_id, Some("mergemaster"), None)
|
.start_agent(&project_root, story_id, Some("mergemaster"), None, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
serde_json::to_string_pretty(&json!({
|
serde_json::to_string_pretty(&json!({
|
||||||
@@ -112,7 +112,7 @@ pub(super) async fn tool_reject_qa(args: &Value, ctx: &AppContext) -> Result<Str
|
|||||||
);
|
);
|
||||||
if let Err(e) = ctx
|
if let Err(e) = ctx
|
||||||
.agents
|
.agents
|
||||||
.start_agent(&project_root, story_id, Some(agent_name), Some(&context))
|
.start_agent(&project_root, story_id, Some(agent_name), Some(&context), None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
slog_warn!("[qa] Failed to restart coder for '{story_id}' after rejection: {e}");
|
slog_warn!("[qa] Failed to restart coder for '{story_id}' after rejection: {e}");
|
||||||
|
|||||||
Reference in New Issue
Block a user