feat(orchestrator): kei pick + spawn_agent MCP tool — true multi-LLM shell
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Closes the "Claude Code as single primary" gap. Now `kei` (no args) execs
whichever CLI is configured as primary, and ANY MCP-capable orchestrator
can spawn KeiSeiKit agents on any backend via the built-in spawn_agent tool.
## A — orchestrator picker
bin/kei now reads ~/.claude/config/primary.toml and execs that CLI instead
of hardcoding claude. New arms:
kei pick interactive menu → set primary → launch it
kei --on=<backend> one-shot launch of <backend> (no primary write)
kei primary [<b>] get/set primary
Splash shows `primary CLI: <backend>` so the orchestrator is visible.
Failure mode: if primary's CLI isn't on PATH, prints install hint + offers
`kei pick` recovery.
scripts/kei-pick.sh — Constructor Pattern picker (<140 LOC). Lists all 6
backends with install status (✓/✗), highlights current primary, writes
choice to primary.toml, execs the picked CLI. Honors stdin TTY gate
(RULE TTY-INTERACTIVITY-GATE — -t 0, not -t 1) for non-interactive safety.
## B — spawn_agent MCP tool
_primitives/_rust/kei-mcp/src/handlers/tools.rs gains a built-in
`spawn_agent` tool, exposed alongside discovered atoms:
- inputSchema: { name: str, task: str, on?: backend-enum }
- Calls kei-agent-cli.sh internally with same DNA resolution
- 60s timeout, kill-on-drop
- Honors KEI_AGENT_CLI env for testing
Smoke 2026-05-26 (MCP stdio JSON-RPC round-trip):
spawn_agent(name=smoke-test, on=claude) → "SMOKE-OK" ✅
spawn_agent(name=smoke-test, on=grok) → "SMOKE-OK" ✅
Why it matters: Claude Code has a native Agent tool. Grok / Agy / Copilot /
Kimi don't have an equivalent native sub-agent surface — but they all speak
MCP. spawn_agent gives them KeiSeiKit's sub-agent capability when they're
the orchestrator. The chosen orchestrator no longer caps the sub-agent fleet.
## Other
_primitives/_rust/kei-mcp/Cargo.toml: tokio gains "io-std" feature (was
missing — main.rs uses tokio::io::stdin/stdout). This fixes a latent build
error unrelated to this PR (kei-mcp wasn't building cleanly before).
Tests: tools_list assertions updated for the +1 built-in tool (3 total
instead of 2 with atoms; 1 instead of 0 on empty root). All MCP tests pass.
Assembler 3/3 golden tests still pass (provider field is optional).
This commit is contained in:
parent
e4980f6ad7
commit
3fec43ea7e
6 changed files with 403 additions and 10 deletions
|
|
@ -18,7 +18,9 @@ path = "src/lib.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true }
|
# v0.39: io-std added for tokio::io::stdin/stdout used by the MCP stdio
|
||||||
|
# transport in main.rs (workspace tokio doesn't enable io-std by default).
|
||||||
|
tokio = { workspace = true, features = ["io-std"] }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
kei-atom-discovery = { path = "../kei-atom-discovery" }
|
kei-atom-discovery = { path = "../kei-atom-discovery" }
|
||||||
kei-skills = { path = "../kei-skills" }
|
kei-skills = { path = "../kei-skills" }
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ pub fn list(req: JsonRpcRequest, ctx: &ServerContext) -> JsonRpcResponse {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(atom_to_tool_descriptor)
|
.map(atom_to_tool_descriptor)
|
||||||
.collect();
|
.collect();
|
||||||
|
// v0.39: built-in spawn_agent tool — exposed to all MCP clients so any
|
||||||
|
// CLI (grok / agy / copilot / kimi / claude) can spawn a KeiSeiKit agent
|
||||||
|
// as a sub-agent. Bypasses atom discovery (it's an internal handler).
|
||||||
|
tools.push(spawn_agent_descriptor());
|
||||||
tools.sort_by(|a, b| {
|
tools.sort_by(|a, b| {
|
||||||
a.get("name").and_then(Value::as_str).unwrap_or("")
|
a.get("name").and_then(Value::as_str).unwrap_or("")
|
||||||
.cmp(b.get("name").and_then(Value::as_str).unwrap_or(""))
|
.cmp(b.get("name").and_then(Value::as_str).unwrap_or(""))
|
||||||
|
|
@ -50,6 +54,18 @@ pub async fn call(req: JsonRpcRequest, ctx: &ServerContext) -> JsonRpcResponse {
|
||||||
None => return err(req.id, INVALID_PARAMS, "missing tool name"),
|
None => return err(req.id, INVALID_PARAMS, "missing tool name"),
|
||||||
};
|
};
|
||||||
let args = params.get("arguments").cloned().unwrap_or(json!({}));
|
let args = params.get("arguments").cloned().unwrap_or(json!({}));
|
||||||
|
|
||||||
|
// v0.39: spawn_agent built-in — short-circuit before atom dispatch.
|
||||||
|
if name == "spawn_agent" {
|
||||||
|
return match invoke_spawn_agent(&args).await {
|
||||||
|
Ok(text) => ok(req.id, json!({
|
||||||
|
"content": [{ "type": "text", "text": text }],
|
||||||
|
"isError": false,
|
||||||
|
})),
|
||||||
|
Err(e) => err(req.id, INTERNAL_ERROR, e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
match invoke_atom(&ctx.atoms_root, &name, &args).await {
|
match invoke_atom(&ctx.atoms_root, &name, &args).await {
|
||||||
Ok(result) => ok(req.id, json!({
|
Ok(result) => ok(req.id, json!({
|
||||||
"content": [{ "type": "text", "text": serde_json::to_string(&result).unwrap_or_default() }],
|
"content": [{ "type": "text", "text": serde_json::to_string(&result).unwrap_or_default() }],
|
||||||
|
|
@ -59,6 +75,94 @@ pub async fn call(req: JsonRpcRequest, ctx: &ServerContext) -> JsonRpcResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// v0.39: built-in `spawn_agent` MCP tool descriptor.
|
||||||
|
/// Exposes KeiSeiKit's cross-CLI agent launcher (`kei-agent-cli.sh`) so any
|
||||||
|
/// MCP client can spawn an agent on any backend (claude / grok / agy /
|
||||||
|
/// copilot / kimi). Solves the "non-claude orchestrator can't natively spawn
|
||||||
|
/// sub-agents" gap — any CLI with MCP support gets the spawn capability.
|
||||||
|
fn spawn_agent_descriptor() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "spawn_agent",
|
||||||
|
"description": "Spawn a KeiSeiKit agent as a sub-agent through any configured LLM CLI backend. Reads ~/.claude/agents/<name>.md, composes with the task, and execs the chosen backend non-interactively. Backend resolution: explicit `on` arg → agent manifest's `provider` → ~/.claude/config/primary.toml → claude.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Agent name (looked up in ~/.claude/agents/<name>.md)"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The task / question to give the agent"
|
||||||
|
},
|
||||||
|
"on": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional explicit backend override (claude/grok/agy/copilot/kimi/codex). Default: DNA → primary → claude.",
|
||||||
|
"enum": ["claude", "grok", "agy", "antigravity", "copilot", "kimi", "codex"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "task"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// v0.39: handler for `tools/call name=spawn_agent`. Shells out to
|
||||||
|
/// `kei-agent-cli.sh` (located via $HOME/.claude/scripts/) and returns
|
||||||
|
/// the backend's stdout as the tool result.
|
||||||
|
async fn invoke_spawn_agent(args: &Value) -> Result<String, String> {
|
||||||
|
let name = args.get("name").and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| "spawn_agent: missing 'name' argument".to_string())?;
|
||||||
|
let task = args.get("task").and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| "spawn_agent: missing 'task' argument".to_string())?;
|
||||||
|
let on_opt = args.get("on").and_then(Value::as_str);
|
||||||
|
|
||||||
|
// Locate the launcher script. Honors KEI_AGENT_CLI override for testing.
|
||||||
|
let script = match std::env::var("KEI_AGENT_CLI") {
|
||||||
|
Ok(v) => PathBuf::from(v),
|
||||||
|
Err(_) => {
|
||||||
|
let home = std::env::var("HOME").map_err(|_| "HOME not set".to_string())?;
|
||||||
|
PathBuf::from(home).join(".claude/scripts/kei-agent-cli.sh")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !script.is_file() {
|
||||||
|
return Err(format!("kei-agent-cli.sh not found: {}", script.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&script);
|
||||||
|
if let Some(on) = on_opt {
|
||||||
|
cmd.arg(format!("--on={on}"));
|
||||||
|
}
|
||||||
|
cmd.arg(name).arg(task);
|
||||||
|
cmd.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.kill_on_drop(true);
|
||||||
|
|
||||||
|
let child = cmd.spawn()
|
||||||
|
.map_err(|e| format!("spawn {}: {e}", script.display()))?;
|
||||||
|
let fut = child.wait_with_output();
|
||||||
|
|
||||||
|
// Reuse the existing ATOM_TIMEOUT_SECS for the spawn_agent cap too —
|
||||||
|
// 60s should suffice for non-interactive prompts; longer tasks would
|
||||||
|
// need streaming, which the MCP tools-call contract doesn't support
|
||||||
|
// anyway. Hung agents are killed at the timeout.
|
||||||
|
match tokio::time::timeout(Duration::from_secs(ATOM_TIMEOUT_SECS), fut).await {
|
||||||
|
Ok(Ok(out)) => {
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
if !out.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||||
|
return Err(format!(
|
||||||
|
"spawn_agent backend exited {}: {stderr}",
|
||||||
|
out.status.code().unwrap_or(-1)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(stdout)
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => Err(format!("wait: {e}")),
|
||||||
|
Err(_) => Err("spawn_agent timeout".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert one atom's metadata into the MCP tool-descriptor shape.
|
/// Convert one atom's metadata into the MCP tool-descriptor shape.
|
||||||
fn atom_to_tool_descriptor(meta: AtomMeta) -> Value {
|
fn atom_to_tool_descriptor(meta: AtomMeta) -> Value {
|
||||||
let description = first_paragraph(&meta.body);
|
let description = first_paragraph(&meta.body);
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,12 @@ async fn tools_list_returns_two_atoms_with_descriptors() {
|
||||||
let resp = dispatch(req, &ctx).await;
|
let resp = dispatch(req, &ctx).await;
|
||||||
let result = resp.result.expect("should have result");
|
let result = resp.result.expect("should have result");
|
||||||
let tools = result["tools"].as_array().expect("tools array");
|
let tools = result["tools"].as_array().expect("tools array");
|
||||||
assert_eq!(tools.len(), 2);
|
// v0.39: list also includes the built-in `spawn_agent` tool (atoms + 1).
|
||||||
|
assert_eq!(tools.len(), 3);
|
||||||
|
assert!(
|
||||||
|
tools.iter().any(|t| t["name"] == "spawn_agent"),
|
||||||
|
"spawn_agent built-in must be present"
|
||||||
|
);
|
||||||
// sorted alphabetically
|
// sorted alphabetically
|
||||||
assert_eq!(tools[0]["name"], "kei-sage::ask");
|
assert_eq!(tools[0]["name"], "kei-sage::ask");
|
||||||
assert_eq!(tools[1]["name"], "kei-task::search");
|
assert_eq!(tools[1]["name"], "kei-task::search");
|
||||||
|
|
@ -91,5 +96,8 @@ async fn tools_list_handles_empty_root() {
|
||||||
};
|
};
|
||||||
let resp = dispatch(req, &ctx).await;
|
let resp = dispatch(req, &ctx).await;
|
||||||
let result = resp.result.expect("should have result");
|
let result = resp.result.expect("should have result");
|
||||||
assert_eq!(result["tools"].as_array().unwrap().len(), 0);
|
// v0.39: empty atoms root still surfaces the built-in `spawn_agent` tool.
|
||||||
|
let tools = result["tools"].as_array().unwrap();
|
||||||
|
assert_eq!(tools.len(), 1);
|
||||||
|
assert_eq!(tools[0]["name"], "spawn_agent");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
bin/kei
77
bin/kei
|
|
@ -10,13 +10,15 @@
|
||||||
# kei --status # status only, don't launch claude
|
# kei --status # status only, don't launch claude
|
||||||
# kei message ... # inter-session mailbox (send/inbox/list) — see kei-message.sh
|
# kei message ... # inter-session mailbox (send/inbox/list) — see kei-message.sh
|
||||||
# kei configure # re-pick stack profile + opt-in hook packs
|
# kei configure # re-pick stack profile + opt-in hook packs
|
||||||
|
# kei pick # interactive picker → set primary → launch it
|
||||||
# kei agent <name> "<task>" # invoke agent, backend from DNA → primary
|
# kei agent <name> "<task>" # invoke agent, backend from DNA → primary
|
||||||
# kei agent --on=<backend> <name> "<task>" # override backend
|
# kei agent --on=<backend> <name> "<task>" # override backend
|
||||||
# kei run-via <backend> <name> "<task>" # invoke agent on explicit backend
|
# kei run-via <backend> <name> "<task>" # invoke agent on explicit backend
|
||||||
# # backends: claude grok agy copilot kimi codex
|
# # backends: claude grok agy copilot kimi codex
|
||||||
# # `kei run-via list` shows install status + agents
|
# # `kei run-via list` shows install status + agents
|
||||||
# kei primary [<backend>] # get/set primary LLM provider (DNA fallback)
|
# kei primary [<backend>] # get/set primary LLM provider (DNA fallback)
|
||||||
# kei [args...] # splash → claude args... (forwarded verbatim)
|
# kei --on=<backend> # one-shot launch of <backend> (does not change primary)
|
||||||
|
# kei [args...] # splash → exec primary CLI (default: claude)
|
||||||
#
|
#
|
||||||
# The splash shows: substrate version, agent count, last sleep run,
|
# The splash shows: substrate version, agent count, last sleep run,
|
||||||
# active sessions (kei-ping). Press any key to skip the dwell.
|
# active sessions (kei-ping). Press any key to skip the dwell.
|
||||||
|
|
@ -53,28 +55,88 @@ case "${1:-}" in
|
||||||
shift
|
shift
|
||||||
exec "$HOME/.claude/scripts/kei-agent-cli.sh" primary "$@"
|
exec "$HOME/.claude/scripts/kei-agent-cli.sh" primary "$@"
|
||||||
;;
|
;;
|
||||||
|
pick)
|
||||||
|
shift
|
||||||
|
exec "$HOME/.claude/scripts/kei-pick.sh" "$@"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# --- one-shot --on=<backend> override (does not write primary.toml) -------
|
||||||
|
ONESHOT_BACKEND=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--on=*) ONESHOT_BACKEND="${arg#--on=}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# --- args ----------------------------------------------------------------
|
# --- args ----------------------------------------------------------------
|
||||||
SPLASH=1
|
SPLASH=1
|
||||||
STATUS_ONLY=0
|
STATUS_ONLY=0
|
||||||
PASSTHROUGH=()
|
PASSTHROUGH=()
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
|
--on=*) ;; # already captured in ONESHOT_BACKEND; don't forward
|
||||||
--no-splash) SPLASH=0 ;;
|
--no-splash) SPLASH=0 ;;
|
||||||
--status) STATUS_ONLY=1; SPLASH=1 ;;
|
--status) STATUS_ONLY=1; SPLASH=1 ;;
|
||||||
*) PASSTHROUGH+=("$arg") ;;
|
*) PASSTHROUGH+=("$arg") ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- locate claude on PATH -----------------------------------------------
|
# --- resolve primary backend ---------------------------------------------
|
||||||
CLAUDE_BIN="$(command -v claude 2>/dev/null || true)"
|
# Order: --on=<backend> override → ~/.claude/config/primary.toml → claude.
|
||||||
if [ -z "$CLAUDE_BIN" ] && [ "$STATUS_ONLY" = "0" ]; then
|
resolve_primary() {
|
||||||
echo "error: 'claude' not on PATH. Install Claude Code first:" >&2
|
if [ -n "$ONESHOT_BACKEND" ]; then printf '%s\n' "$ONESHOT_BACKEND"; return; fi
|
||||||
echo " curl -fsSL https://claude.ai/install.sh | sh" >&2
|
if [ -n "${KEI_PRIMARY:-}" ]; then printf '%s\n' "$KEI_PRIMARY"; return; fi
|
||||||
|
local cfg="$HOME/.claude/config/primary.toml"
|
||||||
|
if [ -f "$cfg" ]; then
|
||||||
|
awk -F'=' '/^provider[[:space:]]*=/ {
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
|
||||||
|
gsub(/^"|"$/, "", $2)
|
||||||
|
print $2; exit
|
||||||
|
}' "$cfg"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
printf 'claude\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Map backend name → executable. Mirrors scripts/kei-agent-cli.sh::backend_bin.
|
||||||
|
backend_bin_for() {
|
||||||
|
case "$1" in
|
||||||
|
claude) echo "claude" ;;
|
||||||
|
grok) echo "grok" ;;
|
||||||
|
agy|antigravity) echo "agy" ;;
|
||||||
|
copilot) echo "copilot" ;;
|
||||||
|
kimi) echo "kimi" ;;
|
||||||
|
codex) echo "codex" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
PRIMARY="$(resolve_primary)"
|
||||||
|
PRIMARY_CLI="$(backend_bin_for "$PRIMARY")" || {
|
||||||
|
echo "error: unknown primary backend: $PRIMARY" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
PRIMARY_BIN="$(command -v "$PRIMARY_CLI" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [ -z "$PRIMARY_BIN" ] && [ "$STATUS_ONLY" = "0" ]; then
|
||||||
|
echo "error: primary backend '$PRIMARY' → '$PRIMARY_CLI' not on PATH." >&2
|
||||||
|
case "$PRIMARY" in
|
||||||
|
claude) echo " install: curl -fsSL https://claude.ai/install.sh | sh" >&2 ;;
|
||||||
|
grok) echo " install: see https://x.ai/grok" >&2 ;;
|
||||||
|
copilot) echo " install: npm i -g @github/copilot" >&2 ;;
|
||||||
|
kimi) echo " install: uv tool install kimi-cli" >&2 ;;
|
||||||
|
agy) echo " install: see https://antigravity.dev" >&2 ;;
|
||||||
|
codex) echo " install: see https://github.com/openai/codex" >&2 ;;
|
||||||
|
esac
|
||||||
|
echo " or: kei pick (interactive picker to choose + set primary)" >&2
|
||||||
|
echo " or: kei primary <backend> (set a different default)" >&2
|
||||||
exit 127
|
exit 127
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Legacy var name for splash code below.
|
||||||
|
CLAUDE_BIN="$PRIMARY_BIN"
|
||||||
|
|
||||||
# --- read state ----------------------------------------------------------
|
# --- read state ----------------------------------------------------------
|
||||||
AGENTS_DIR="${HOME}/.claude/agents"
|
AGENTS_DIR="${HOME}/.claude/agents"
|
||||||
SYNC_DIR="${HOME}/.claude/memory/sync-repo"
|
SYNC_DIR="${HOME}/.claude/memory/sync-repo"
|
||||||
|
|
@ -155,8 +217,9 @@ ${C1} ██╔═██╗ ██╔══╝ ██║╚════█
|
||||||
${C1} ██║ ██╗███████╗██║███████║███████╗██║${C0}
|
${C1} ██║ ██╗███████╗██║███████║███████╗██║${C0}
|
||||||
${C1} ╚═╝ ╚═╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝${C0}
|
${C1} ╚═╝ ╚═╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝${C0}
|
||||||
|
|
||||||
${C2} KeiSeiKit · substrate v0.38${C0}
|
${C2} KeiSeiKit · substrate v0.39${C0}
|
||||||
${C3} ─────────────────────────────────────${C0}
|
${C3} ─────────────────────────────────────${C0}
|
||||||
|
primary CLI : ${CV}${PRIMARY}${C0}
|
||||||
profile : ${CV}${p}${C0}
|
profile : ${CV}${p}${C0}
|
||||||
agents : ${CV}${ac}${C0}
|
agents : ${CV}${ac}${C0}
|
||||||
last sleep run : ${CV}${sl}${C0}
|
last sleep run : ${CV}${sl}${C0}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,69 @@ strengths; the substrate is agnostic about which you pick. Pick by:
|
||||||
- **Independent second opinion** — same agent, different model, see if
|
- **Independent second opinion** — same agent, different model, see if
|
||||||
conclusions diverge.
|
conclusions diverge.
|
||||||
|
|
||||||
|
## Orchestrator picker — `kei` no longer hardcodes claude
|
||||||
|
|
||||||
|
Without args, `kei` reads `~/.claude/config/primary.toml` and execs that CLI.
|
||||||
|
The picker lets you change it interactively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kei pick # interactive menu → set primary → launch it
|
||||||
|
kei # splash → exec the configured primary
|
||||||
|
kei --on=grok # one-shot launch of grok (does NOT change primary)
|
||||||
|
kei primary grok # set default to grok (no launch)
|
||||||
|
kei primary # show current primary
|
||||||
|
```
|
||||||
|
|
||||||
|
The splash shows `primary CLI: <backend>` so you always know which orchestrator
|
||||||
|
will start. If the chosen primary isn't installed, `kei` prints the install
|
||||||
|
command and offers `kei pick` as recovery.
|
||||||
|
|
||||||
|
## Cross-CLI sub-agent spawn via MCP — `spawn_agent`
|
||||||
|
|
||||||
|
`kei-mcp` exposes a built-in `spawn_agent` MCP tool. Any CLI that connects
|
||||||
|
to it as an MCP client can invoke KeiSeiKit agents on any backend, no matter
|
||||||
|
what the orchestrator is:
|
||||||
|
|
||||||
|
```jsonrpc
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "spawn_agent",
|
||||||
|
"arguments": {
|
||||||
|
"name": "critic",
|
||||||
|
"task": "review src/auth.rs for race conditions",
|
||||||
|
"on": "grok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally `spawn_agent` shells out to `kei-agent-cli.sh` with the same DNA
|
||||||
|
resolution as `kei agent`. The `on` argument is optional — without it, the
|
||||||
|
backend is picked from the agent's manifest, then `primary.toml`, then claude.
|
||||||
|
|
||||||
|
**Why this matters:** Claude Code has a native `Agent` tool for sub-agent
|
||||||
|
spawning. Grok / Antigravity / Copilot / Kimi do NOT have that surface
|
||||||
|
natively — but they all support MCP. With `spawn_agent` exposed via kei-mcp,
|
||||||
|
**every backend that speaks MCP gets KeiSeiKit's sub-agent capability**. So
|
||||||
|
when Grok is your orchestrator, it can still spawn `critic` on Claude (or
|
||||||
|
`code-implementer` on Antigravity, or anything else) — the orchestrator
|
||||||
|
choice no longer caps your sub-agent surface.
|
||||||
|
|
||||||
|
Wire kei-mcp into the orchestrator's MCP config (each CLI has its own):
|
||||||
|
|
||||||
|
| CLI | MCP config |
|
||||||
|
|---|---|
|
||||||
|
| claude | `~/.claude/settings.json` `mcpServers` block |
|
||||||
|
| grok | `~/.grok/config.json` (or check `grok --help`) |
|
||||||
|
| agy | `~/.antigravity/mcp.json` (check `agy plugin list`) |
|
||||||
|
| copilot | `~/.copilot/mcp.json` (check `copilot --help`) |
|
||||||
|
| kimi | `kimi mcp add` subcommand |
|
||||||
|
|
||||||
|
Point each at `<kit>/_primitives/_rust/target/release/kei-mcp` (built via
|
||||||
|
`cargo build -p kei-mcp --release`).
|
||||||
|
|
||||||
## Rule enforcement caveat (READ THIS)
|
## Rule enforcement caveat (READ THIS)
|
||||||
|
|
||||||
KeiSeiKit hooks (`numeric-claims-guard`, `citation-verify`, `no-github-push`,
|
KeiSeiKit hooks (`numeric-claims-guard`, `citation-verify`, `no-github-push`,
|
||||||
|
|
|
||||||
153
scripts/kei-pick.sh
Executable file
153
scripts/kei-pick.sh
Executable file
|
|
@ -0,0 +1,153 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# kei-pick — interactive orchestrator picker.
|
||||||
|
#
|
||||||
|
# Shows installed LLM CLIs, lets the user choose one, writes it to
|
||||||
|
# ~/.claude/config/primary.toml, then exec's it (so the shell becomes
|
||||||
|
# the picked orchestrator). Designed for `kei pick`.
|
||||||
|
#
|
||||||
|
# Non-interactive (no TTY): just shows status and exits 0.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
KEI_PRIMARY_CFG="${KEI_PRIMARY_CFG:-$HOME/.claude/config/primary.toml}"
|
||||||
|
|
||||||
|
# Mirrors scripts/kei-agent-cli.sh::backend_bin and bin/kei::backend_bin_for.
|
||||||
|
backend_bin() {
|
||||||
|
case "$1" in
|
||||||
|
claude) echo "claude" ;;
|
||||||
|
grok) echo "grok" ;;
|
||||||
|
agy|antigravity) echo "agy" ;;
|
||||||
|
copilot) echo "copilot" ;;
|
||||||
|
kimi) echo "kimi" ;;
|
||||||
|
codex) echo "codex" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_label() {
|
||||||
|
case "$1" in
|
||||||
|
claude) echo "Claude Code (Anthropic)" ;;
|
||||||
|
grok) echo "Grok Build TUI (xAI)" ;;
|
||||||
|
agy) echo "Antigravity / Gemini (Google)" ;;
|
||||||
|
copilot) echo "GitHub Copilot CLI (Microsoft/GitHub)" ;;
|
||||||
|
kimi) echo "Kimi Code CLI (Moonshot) — TUI-only for agents" ;;
|
||||||
|
codex) echo "Codex CLI (OpenAI)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
current_primary() {
|
||||||
|
[ -f "$KEI_PRIMARY_CFG" ] || { echo "claude"; return; }
|
||||||
|
awk -F'=' '/^provider[[:space:]]*=/ {
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
|
||||||
|
gsub(/^"|"$/, "", $2)
|
||||||
|
print $2; exit
|
||||||
|
}' "$KEI_PRIMARY_CFG"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- list installed backends ------------------------------------------
|
||||||
|
BACKENDS=(claude grok agy copilot kimi codex)
|
||||||
|
INSTALLED=()
|
||||||
|
for b in "${BACKENDS[@]}"; do
|
||||||
|
bin=$(backend_bin "$b")
|
||||||
|
if command -v "$bin" >/dev/null 2>&1; then
|
||||||
|
INSTALLED+=("$b")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cur=$(current_primary)
|
||||||
|
|
||||||
|
# --- non-interactive: just show status --------------------------------
|
||||||
|
# Gate on stdin (RULE TTY-INTERACTIVITY-GATE): -t 0, not -t 1.
|
||||||
|
# curl|bash tees stdout, so -t 1 false ≠ non-interactive.
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
echo "kei pick — non-interactive mode"
|
||||||
|
echo "current primary: $cur"
|
||||||
|
echo "installed backends: ${INSTALLED[*]:-none}"
|
||||||
|
echo "(run \`kei pick\` from a real terminal for the picker)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- interactive picker -----------------------------------------------
|
||||||
|
C0="" CB="" CC="" CD=""
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
C0=$'\033[0m'
|
||||||
|
CB=$'\033[1;38;5;39m' # blue
|
||||||
|
CC=$'\033[1;38;5;220m' # gold
|
||||||
|
CD=$'\033[2m' # dim
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
${CB}╔════════════════════════════════════════════╗
|
||||||
|
║ KeiSeiKit · orchestrator picker ║
|
||||||
|
╚════════════════════════════════════════════╝${C0}
|
||||||
|
|
||||||
|
Pick the LLM CLI that becomes your primary shell.
|
||||||
|
Any agent invocation (\`kei agent <name>\`) routes here unless DNA overrides.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
i=1
|
||||||
|
for b in "${BACKENDS[@]}"; do
|
||||||
|
bin=$(backend_bin "$b")
|
||||||
|
label=$(backend_label "$b")
|
||||||
|
if command -v "$bin" >/dev/null 2>&1; then
|
||||||
|
mark="${CC}✓${C0}"
|
||||||
|
else
|
||||||
|
mark="${CD}✗${C0}"
|
||||||
|
label="$label ${CD}(not installed)${C0}"
|
||||||
|
fi
|
||||||
|
cur_mark=""
|
||||||
|
[ "$b" = "$cur" ] && cur_mark="${CC} ← current${C0}"
|
||||||
|
printf " ${CB}%d${C0}) %s %-10s %s%s\n" "$i" "$mark" "$b" "$label" "$cur_mark"
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
printf " ${CB}q${C0}) cancel (keep current: ${CC}%s${C0})\n\n" "$cur"
|
||||||
|
printf "Pick [1-${#BACKENDS[@]}/q]: "
|
||||||
|
read -r choice
|
||||||
|
choice="${choice:-q}"
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
q|Q|"") echo "cancelled."; exit 0 ;;
|
||||||
|
[1-9])
|
||||||
|
idx=$((choice-1))
|
||||||
|
if [ $idx -ge ${#BACKENDS[@]} ] || [ $idx -lt 0 ]; then
|
||||||
|
echo "invalid choice: $choice" >&2; exit 2
|
||||||
|
fi
|
||||||
|
new="${BACKENDS[$idx]}"
|
||||||
|
;;
|
||||||
|
*) echo "invalid choice: $choice" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
bin=$(backend_bin "$new")
|
||||||
|
if ! command -v "$bin" >/dev/null 2>&1; then
|
||||||
|
echo
|
||||||
|
echo "${CC}'$new' is not installed.${C0}"
|
||||||
|
echo "Set as primary anyway (you'll need to install it before \`kei\` will work)? [y/N]: "
|
||||||
|
read -r confirm
|
||||||
|
case "$confirm" in y|Y|yes) ;; *) echo "cancelled."; exit 0 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$KEI_PRIMARY_CFG")"
|
||||||
|
printf '# kei primary — written %s\nprovider = "%s"\n' \
|
||||||
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$new" > "$KEI_PRIMARY_CFG"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "${CC}✓${C0} primary set: $cur → ${CC}$new${C0}"
|
||||||
|
echo " config: $KEI_PRIMARY_CFG"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ -n "${KEI_NO_LAUNCH:-}" ]; then
|
||||||
|
echo "(skipping launch — KEI_NO_LAUNCH set)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v "$bin" >/dev/null 2>&1; then
|
||||||
|
echo "${CD}skipping launch — $bin not on PATH; install it then run \`kei\`.${C0}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "launching $new..."
|
||||||
|
exec "$bin"
|
||||||
Loading…
Reference in a new issue