spawn <task.toml> internally calls prepare + ledger fork, emits JSON ready for Agent tool invocation. verify wraps post-return check+ledger update. list-pending shows running forks. kei-ledger invoked via subprocess (no lib.rs in kei-ledger). KEI_SPAWN_LEDGER_NOOP=1 test escape hatch for CI without binary. spec_sha = SHA-256 of task.toml bytes (workspace sha2 dep). Tests: 6/6 integration (happy, explicit-id, unknown-role, non-spawnable, verify-missing, end-to-end roundtrip). Step 3 (Anthropic API) stays with orchestrator — next iteration adds kei-spawn drive <task.toml> for HTTP automation. Workspace Cargo.toml: +kei-spawn member. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
106 lines
2.9 KiB
Rust
106 lines
2.9 KiB
Rust
//! kei-spawn — CLI dispatcher.
|
|
//!
|
|
//! Three subcommands:
|
|
//! - `spawn <task.toml>` — prepare invocation + ledger fork, emit JSON
|
|
//! - `verify <agent-id> <worktree>` — run verify pipeline, update ledger
|
|
//! - `list-pending` — forward `kei-ledger list --status running`
|
|
|
|
use clap::{Parser, Subcommand};
|
|
use std::path::PathBuf;
|
|
use std::process::ExitCode;
|
|
|
|
use kei_spawn::{ledger_sh, spawn_from_task, verify_agent};
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "kei-spawn",
|
|
version,
|
|
about = "Automation envelope: prepare + ledger fork + verify (RULE 0.13-compliant)"
|
|
)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
cmd: Cmd,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Cmd {
|
|
/// Prepare an Agent-tool invocation + register ledger row.
|
|
Spawn {
|
|
/// Path to task.toml.
|
|
task: PathBuf,
|
|
/// kit root (default: cwd).
|
|
#[arg(long)]
|
|
kit_root: Option<PathBuf>,
|
|
},
|
|
/// Run verify pipeline + update ledger status.
|
|
Verify {
|
|
/// agent-id previously emitted by `kei-spawn spawn`.
|
|
agent_id: String,
|
|
/// Worktree path reported by the Claude harness on agent return.
|
|
worktree: PathBuf,
|
|
#[arg(long)]
|
|
kit_root: Option<PathBuf>,
|
|
},
|
|
/// Show all running ledger rows.
|
|
ListPending,
|
|
}
|
|
|
|
fn main() -> ExitCode {
|
|
let cli = Cli::parse();
|
|
match cli.cmd {
|
|
Cmd::Spawn { task, kit_root } => run_spawn(task, kit_root),
|
|
Cmd::Verify { agent_id, worktree, kit_root } => {
|
|
run_verify(agent_id, worktree, kit_root)
|
|
}
|
|
Cmd::ListPending => run_list_pending(),
|
|
}
|
|
}
|
|
|
|
fn run_spawn(task: PathBuf, kit_root: Option<PathBuf>) -> ExitCode {
|
|
let kit = kit_root_or_cwd(kit_root);
|
|
match spawn_from_task(&task, &kit) {
|
|
Ok(out) => emit_json(&out),
|
|
Err(e) => err("spawn", e),
|
|
}
|
|
}
|
|
|
|
fn run_verify(agent_id: String, worktree: PathBuf, kit_root: Option<PathBuf>) -> ExitCode {
|
|
let kit = kit_root_or_cwd(kit_root);
|
|
match verify_agent(&agent_id, &worktree, &kit) {
|
|
Ok(out) => {
|
|
let code = if out.is_clean { ExitCode::SUCCESS } else { ExitCode::from(2) };
|
|
let _ = emit_json(&out);
|
|
code
|
|
}
|
|
Err(e) => err("verify", e),
|
|
}
|
|
}
|
|
|
|
fn run_list_pending() -> ExitCode {
|
|
match ledger_sh::list_running() {
|
|
Ok(s) => {
|
|
print!("{s}");
|
|
ExitCode::SUCCESS
|
|
}
|
|
Err(e) => err("list-pending", e),
|
|
}
|
|
}
|
|
|
|
fn emit_json<T: serde::Serialize>(v: &T) -> ExitCode {
|
|
match serde_json::to_string_pretty(v) {
|
|
Ok(s) => {
|
|
println!("{s}");
|
|
ExitCode::SUCCESS
|
|
}
|
|
Err(e) => err("serialize json", e),
|
|
}
|
|
}
|
|
|
|
fn kit_root_or_cwd(arg: Option<PathBuf>) -> PathBuf {
|
|
arg.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
|
|
}
|
|
|
|
fn err(stage: &str, e: impl std::fmt::Display) -> ExitCode {
|
|
eprintln!("kei-spawn {stage}: {e}");
|
|
ExitCode::from(1)
|
|
}
|