Harden worker boot recovery before task dispatch

The worker boot registry now exposes the requested lifecycle states, emits structured trust and prompt-delivery events, and recovers from shell or wrong-target prompt delivery by replaying the last prompt. Supporting fixes keep MCP remote config parsing backwards-compatible and make CLI argument parsing less dependent on ambient config and cwd state so the workspace stays green under full parallel test runs.

Constraint: Worker prompts must not be dispatched before a confirmed ready_for_prompt handshake
Constraint: Prompt misdelivery recovery must stay minimal and avoid new dependencies
Rejected: Keep prompt_accepted and blocked as public lifecycle states | user requested the narrower explicit state set
Rejected: Treat url-only MCP server configs as invalid | existing CLI/runtime tests still rely on that shorthand
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve prompt_in_flight semantics when extending worker boot; misdelivery detection depends on it
Tested: cargo build --workspace; cargo test --workspace
Not-tested: Live tmux worker delivery against a real external coding agent pane
This commit is contained in:
Yeachan-Heo
2026-04-04 14:50:31 +00:00
parent d87fbe6c65
commit 784f07abfa
6 changed files with 398 additions and 88 deletions

View File

@@ -212,7 +212,7 @@ impl CliOutputFormat {
fn parse_args(args: &[String]) -> Result<CliAction, String> {
let mut model = DEFAULT_MODEL.to_string();
let mut output_format = CliOutputFormat::Text;
let mut permission_mode = default_permission_mode();
let mut permission_mode_override = None;
let mut wants_help = false;
let mut wants_version = false;
let mut allowed_tool_values = Vec::new();
@@ -251,7 +251,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let value = args
.get(index + 1)
.ok_or_else(|| "missing value for --permission-mode".to_string())?;
permission_mode = parse_permission_mode_arg(value)?;
permission_mode_override = Some(parse_permission_mode_arg(value)?);
index += 2;
}
flag if flag.starts_with("--output-format=") => {
@@ -259,11 +259,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
index += 1;
}
flag if flag.starts_with("--permission-mode=") => {
permission_mode = parse_permission_mode_arg(&flag[18..])?;
permission_mode_override = Some(parse_permission_mode_arg(&flag[18..])?);
index += 1;
}
"--dangerously-skip-permissions" => {
permission_mode = PermissionMode::DangerFullAccess;
permission_mode_override = Some(PermissionMode::DangerFullAccess);
index += 1;
}
"-p" => {
@@ -277,7 +277,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
model: resolve_model_alias(&model).to_string(),
output_format,
allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
permission_mode,
permission_mode: permission_mode_override
.unwrap_or_else(default_permission_mode),
});
}
"--print" => {
@@ -330,6 +331,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
if rest.is_empty() {
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
return Ok(CliAction::Repl {
model,
allowed_tools,
@@ -339,10 +341,13 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
if rest.first().map(String::as_str) == Some("--resume") {
return parse_resume_args(&rest[1..]);
}
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode) {
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode_override)
{
return action;
}
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
match rest[0].as_str() {
"dump-manifests" => Ok(CliAction::DumpManifests),
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
@@ -386,7 +391,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
fn parse_single_word_command_alias(
rest: &[String],
model: &str,
permission_mode: PermissionMode,
permission_mode_override: Option<PermissionMode>,
) -> Option<Result<CliAction, String>> {
if rest.len() != 1 {
return None;
@@ -397,7 +402,7 @@ fn parse_single_word_command_alias(
"version" => Some(Ok(CliAction::Version)),
"status" => Some(Ok(CliAction::Status {
model: model.to_string(),
permission_mode,
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
})),
"sandbox" => Some(Ok(CliAction::Sandbox)),
other => bare_slash_command_guidance(other).map(Err),
@@ -588,6 +593,9 @@ fn resolve_model_alias(model: &str) -> &str {
}
fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
if values.is_empty() {
return Ok(None);
}
current_tool_registry()?.normalize_allowed_tools(values)
}
@@ -5718,11 +5726,17 @@ mod tests {
}
fn with_current_dir<T>(cwd: &Path, f: impl FnOnce() -> T) -> T {
let _guard = cwd_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = std::env::current_dir().expect("cwd should load");
std::env::set_current_dir(cwd).expect("cwd should change");
let result = f();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
std::env::set_current_dir(previous).expect("cwd should restore");
result
match result {
Ok(value) => value,
Err(payload) => std::panic::resume_unwind(payload),
}
}
fn write_plugin_fixture(root: &Path, name: &str, include_hooks: bool, include_lifecycle: bool) {