KeiSeiKit-1.0/_primitives/_rust/kei-spawn/src/main.rs
Parfii-bot 02451f5f49 feat(sp1): NEW kei-spawn crate — automation envelope
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>
2026-04-23 10:21:45 +08:00

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)
}