feat(dna): provider+model in agent DNA; kei primary; smoke-tested 4/5 CLIs
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

Makes KeiSeiKit truly multi-LLM: any agent can declare its preferred backend
in its manifest. The DNA resolver picks the right CLI; `kei primary` swaps the
fleet-wide default. KeiSeiKit is no longer tied to Claude Code single-model.

Resolution order: --on=<backend>  →  manifest provider  →  primary.toml  →  claude

Files:
  _assembler/src/manifest.rs   + Option<String> provider field
  _assembler/src/assembler.rs  emit provider: in frontmatter (when set)
  scripts/kei-agent-cli.sh     DNA resolver; `kei primary` get/set; `kei agent`
                               arm (DNA-driven); honest kimi handling (TUI-only)
  bin/kei                      new arms: agent, primary
  _primitives/cli-backends.toml mark kimi as tui-only
  docs/encyclopedia/multi-cli-agents.md  rewritten with DNA flow, smoke
                               results, rule-enforcement caveat

Smoke 2026-05-26 (real CLI invocations):
  claude   ✓ via `claude -p`
  grok     ✓ via `grok --print`            (DNA: manifest provider=grok)
  agy      ✓ via `agy --print`             (Antigravity / Gemini)
  copilot  ✓ via `copilot --prompt`        (1 Premium / 9s / 20.6k tok)
  kimi     ⚠ TUI-only, no print mode; need `kimi acp` JSON-RPC client
  codex    — register-only (not installed locally)

Rule-enforcement caveat documented: KeiSeiKit hooks fire only inside Claude
Code's PreToolUse pipeline. Non-claude backends carry the agent's PROMPT but
not the hook layer. For tool-level policy on non-claude, route through MCP.

ALSO: fix(stop-hook) — RULE 0.14 session-end-dump.sh "Recombobulating..."
4-minute hang on 18MB+ transcripts. Root cause: kei-memory ingest + frustration-
matrix scan + kei-sleep-sync ran sync at session end. Now async-detached with
per-op portable timeout (timeout/gtimeout/perl alarm). Hook returns in 0.03s.
Raw JSONL saved sync; only index/embedding step deferred (idempotent on
session_id so safe).
This commit is contained in:
KeiSei84 2026-05-26 16:21:11 +08:00
parent 3be9a8bf71
commit e4980f6ad7
7 changed files with 335 additions and 93 deletions

View file

@ -45,6 +45,12 @@ fn write_frontmatter(m: &Manifest, out: &mut String) {
out.push_str(&format!("description: {}\n", desc.trim()));
out.push_str(&format!("tools: {}\n", m.tools.join(", ")));
out.push_str(&format!("model: {}\n", m.model));
// v0.39: optional provider for DNA-resolved kei agent dispatch.
if let Some(prov) = &m.provider {
if !prov.is_empty() {
out.push_str(&format!("provider: {}\n", prov));
}
}
out.push_str("---\n\n");
out.push_str(&format!(
"<!-- GENERATED by _assembler (Rust) from _manifests/{}.toml — DO NOT EDIT. Edit the manifest. -->\n\n",

View file

@ -9,6 +9,13 @@ pub struct Manifest {
pub description: String,
pub tools: Vec<String>,
pub model: String,
/// v0.39 (multi-CLI): optional LLM provider this agent prefers when invoked
/// via `kei agent <name>`. Values: claude / grok / agy / copilot / kimi /
/// codex. Empty / missing → DNA resolver falls back to ~/.claude/config/
/// primary.toml, then to claude. Affects `kei run-via` / `kei agent`
/// dispatch; does NOT change Claude Code's in-session model.
#[serde(default)]
pub provider: Option<String>,
pub role: String,
pub blocks: Vec<String>,
/// v0.16 (phase 5): agent substrate role. When present, assembler loads

View file

@ -34,8 +34,8 @@ homepage = "https://github.com/github/copilot-cli"
[backend.kimi]
bin = "kimi"
prompt_flag = "stdin"
notes = "Moonshot Kimi CLI — primarily TUI/ACP; non-interactive via stdin"
prompt_flag = "tui-only"
notes = "Moonshot Kimi CLI — TUI-ONLY (smoke 2026-05-26). Headless requires ACP client; launcher saves prompt to tmpfile + opens TUI for paste."
homepage = "https://moonshotai.github.io/kimi-cli/"
[backend.codex]

26
bin/kei
View file

@ -10,10 +10,12 @@
# kei --status # status only, don't launch claude
# kei message ... # inter-session mailbox (send/inbox/list) — see kei-message.sh
# kei configure # re-pick stack profile + opt-in hook packs
# kei run-via <backend> <agent> "<task>"
# # invoke a KeiSeiKit agent via an external LLM CLI
# kei agent <name> "<task>" # invoke agent, backend from DNA → primary
# kei agent --on=<backend> <name> "<task>" # override backend
# kei run-via <backend> <name> "<task>" # invoke agent on explicit backend
# # backends: claude grok agy copilot kimi codex
# # `kei run-via list` shows install status
# # `kei run-via list` shows install status + agents
# kei primary [<backend>] # get/set primary LLM provider (DNA fallback)
# kei [args...] # splash → claude args... (forwarded verbatim)
#
# The splash shows: substrate version, agent count, last sleep run,
@ -24,8 +26,12 @@
set -e
# --- subcommand dispatch (before splash) ---------------------------------
# `kei message ...` → mailbox CLI; `kei configure` → hook/stack re-picker;
# `kei run-via ...` → invoke agent through external LLM CLI; rest = launch.
# `kei message ...` → mailbox CLI
# `kei configure` → hook/stack re-picker
# `kei agent ...` → DNA-resolved agent (manifest provider → primary → claude)
# `kei run-via ...` → explicit-backend agent invocation
# `kei primary ...` → get/set primary LLM provider
# rest = splash + launch claude (legacy primary).
case "${1:-}" in
message|msg|m)
shift
@ -35,10 +41,18 @@ case "${1:-}" in
shift
exec "$HOME/.claude/scripts/kei-configure.sh" "$@"
;;
run-via|via|run|agent-via)
agent)
shift
exec "$HOME/.claude/scripts/kei-agent-cli.sh" "$@"
;;
run-via|via|agent-via)
shift
exec "$HOME/.claude/scripts/kei-agent-cli.sh" "$@"
;;
primary)
shift
exec "$HOME/.claude/scripts/kei-agent-cli.sh" primary "$@"
;;
esac
# --- args ----------------------------------------------------------------

View file

@ -1,53 +1,99 @@
# Multi-CLI agent invocation
> *Cross-LLM agent execution. Same agent definition, different backend.*
> *Same DNA, swap the brain. KeiSeiKit is no longer Claude-Code-only.*
KeiSeiKit agents are markdown files. Any LLM CLI that takes a prompt can
host them — `kei run-via` is the launcher that bridges them.
## Backends
Registered in `_primitives/cli-backends.toml` (SSoT). Installed locally
via your own subscription / package manager:
| Backend | CLI binary | Non-interactive flag | Native `--agent` | Notes |
|----------|-----------|----------------------|------------------|-------|
| claude | `claude` | `-p` | yes | Claude Code (Anthropic) |
| grok | `grok` | `--print` | yes | xAI Grok Build TUI |
| agy | `agy` | `--print` | no | Google Antigravity (alias: `antigravity`) |
| copilot | `copilot` | `--prompt` | no | GitHub Copilot CLI (`@github/copilot`) |
| kimi | `kimi` | stdin | no | Moonshot Kimi (primarily TUI/ACP) |
| codex | `codex` | `-p` | no | OpenAI Codex (register-only) |
Run `kei run-via list` to see which are installed on the current machine
and to list available agent names.
## Usage
host them. Three call shapes:
```bash
# Invoke the 'critic' agent through Grok with a task:
kei run-via grok critic "review src/auth.rs for variant analysis"
kei agent <name> "<task>" # DNA-resolved (manifest → primary → claude)
kei agent --on=<backend> <name> "<task>" # override DNA
kei run-via <backend> <name> "<task>" # explicit backend (no DNA lookup)
```
# Same agent, different backend:
kei run-via agy critic "review src/auth.rs"
kei run-via copilot critic "review src/auth.rs"
kei run-via claude critic "review src/auth.rs"
## Backends — smoke-tested 2026-05-26
# Point at an arbitrary agent .md (not in ~/.claude/agents/):
kei run-via grok --file=/tmp/my-agent.md "do the thing"
| Backend | CLI | Flag | Smoke | Notes |
|----------|-----------|--------------|-------|-------|
| claude | `claude` | `-p` | ✅ | Claude Code, native `--agent` flag |
| grok | `grok` | `--print` | ✅ | xAI Grok Build TUI, native `--agent` flag |
| agy | `agy` | `--print` | ✅ | Google Antigravity (Gemini models). Alias: `antigravity` |
| copilot | `copilot` | `--prompt` | ✅ | GitHub Copilot CLI (`@github/copilot`) |
| kimi | `kimi` | TUI-only | ⚠ | No print mode — launcher saves prompt to tmpfile + opens TUI for paste. `kimi acp` JSON-RPC integration is future work. |
| codex | `codex` | `-p` | — | OpenAI Codex (register-only; not installed locally) |
# Backend's native --agent flag (grok/claude only):
KEI_NATIVE_AGENT=1 kei run-via grok critic "review src/auth.rs"
Run `kei run-via list` to see installed backends, current primary, and agent names.
## DNA — agent prefers a provider
Add `provider` to the agent manifest:
```toml
# _manifests/my-agent.toml
name = "my-agent"
provider = "grok" # preferred backend; optional
model = "grok-2" # advisory; informs choice but not yet sent through
```
The assembler emits it into frontmatter:
```yaml
---
name: my-agent
provider: grok
---
```
Resolution order (each falls through if previous returns nothing):
1. `--on=<backend>` flag on the command line
2. `provider:` field in agent manifest
3. `~/.claude/config/primary.toml` (set via `kei primary <backend>`)
4. Default: `claude`
## Primary — your default LLM
```bash
kei primary # show current primary (and fallback)
kei primary grok # set default to Grok
kei primary claude # back to Claude Code
```
`kei primary` writes `~/.claude/config/primary.toml`. Any agent without
its own `provider:` field will resolve to this. This is the lever to
"swap out Claude Code as the primary shell" — set primary to grok, and
every `kei agent <name>` runs on Grok.
## Usage examples
```bash
# DNA mode (manifest's provider, or primary, or claude):
kei agent critic "review src/auth.rs"
# Override DNA — try the same agent on a different model for a second opinion:
kei agent --on=grok critic "review src/auth.rs"
kei agent --on=agy critic "review src/auth.rs"
kei agent --on=copilot critic "review src/auth.rs"
# Explicit backend, no DNA lookup (legacy):
kei run-via grok critic "review src/auth.rs"
# Point at an arbitrary agent file:
kei agent --on=grok --file=/tmp/my-agent.md "do the thing"
# Native --agent flag (grok/claude only):
KEI_NATIVE_AGENT=1 kei agent critic "review src/auth.rs"
```
## How it works
1. Reads `~/.claude/agents/<agent-name>.md` (assembler-generated prompt).
2. Strips YAML frontmatter.
3. Composes with task as: `<agent prompt>\n\n---\n\nTASK FOR THIS RUN:\n<task>`.
4. Execs the backend's non-interactive CLI with the composed prompt.
1. Resolves backend from DNA (see above).
2. Reads `~/.claude/agents/<agent-name>.md` (assembler-generated prompt).
3. Strips YAML frontmatter.
4. Composes with task: `<agent prompt>\n\n---\n\nTASK FOR THIS RUN:\n<task>`.
5. Execs the backend's non-interactive CLI with the composed prompt.
No agent file is modified. No new tokens are issued. Subscription
No agent file is modified. No new tokens are issued — subscription
authentication is whatever each CLI uses (its own login / config dir).
## When to use each
@ -62,18 +108,40 @@ strengths; the substrate is agnostic about which you pick. Pick by:
- **Independent second opinion** — same agent, different model, see if
conclusions diverge.
## Rule enforcement caveat (READ THIS)
KeiSeiKit hooks (`numeric-claims-guard`, `citation-verify`, `no-github-push`,
`safety-guard`, `push-to-main`, etc.) are **Claude Code-side**:
`PreToolUse:Bash` / `:Edit` / `:Write` events that fire inside Claude Code's
process. They do **not** propagate to grok / agy / copilot / kimi.
That means:
- **Prompt-level rules** (the agent's instructions inside the `.md`) DO
carry through — the agent reads Constructor Pattern, Evidence Grading,
No Hallucination, etc. as part of its system prompt on any backend.
- **Tool-level enforcement** (hard-deny on `git push github.com`,
citation guard, etc.) only applies on the **claude** backend. Other
backends' tool surfaces are governed by THEIR own hooks/policies.
If you need true rule-enforcement on a non-claude backend, the path is
the **MCP server** (`_primitives/_rust/kei-mcp/`): registers KeiSeiKit
primitives as MCP tools that the other CLI invokes. Tool-side policies
travel with the MCP wrapper, not with the CLI.
## Adding a new backend
1. Add a `[backend.<name>]` table to `_primitives/cli-backends.toml`.
2. Add a case arm in `scripts/kei-agent-cli.sh` `backend_bin()` and
`backend_invoke()` for the new CLI's print-flag.
3. Add a row to the table above.
3. Add a row to the smoke-test table above (state PASS/FAIL/PARTIAL).
## What it is NOT
- Not a router — picks no backend for you; you ask, it dispatches.
- Not a router — picks no backend for you; you (or DNA) ask, it dispatches.
- Not a federation — each backend runs independently with its own
context; there is no cross-backend state.
- Not a rule-enforcement layer — hooks only fire on the claude backend
(see caveat above). For non-claude rule enforcement use MCP server.
- Not a wrapper around the backend's tool surface — what the CLI can
do (Bash, file edits, MCP, etc.) is determined by that CLI, not
KeiSeiKit. The substrate only ships the prompt.

View file

@ -36,36 +36,67 @@ if [ -n "$transcript" ] && [ -f "$transcript" ]; then
cp -f "$transcript" "$dest" 2>/dev/null || true
fi
# Best-effort ingest — advisory only; never blocks the session from ending.
# RECURRENCE FIX 2026-05-26: 18MB+ transcripts caused 4-minute "Recombobulating…"
# hangs at session end. The three heavy ops below now run async-detached:
# hook returns immediately, ingest / scan / sync grind in background.
# Raw JSONL is already saved sync (line 36) — no data loss; only the
# index/embedding step is deferred. kei-memory ingest is idempotent on
# session_id so partial runs are safe.
bg_log="${HOME}/.claude/memory/traces/session-end.bg.log"
mkdir -p "$(dirname "$bg_log")" 2>/dev/null || true
# Portable timeout (macOS has no `timeout` / `gtimeout` by default).
# Fallback: perl alarm. Final fallback: no timeout (rely on detach).
kei_with_timeout() {
secs="$1"; shift
if command -v timeout >/dev/null 2>&1; then
timeout "$secs" "$@"
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$secs" "$@"
elif command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift @ARGV; exec @ARGV' "$secs" "$@"
else
"$@"
fi
}
# Best-effort ingest — async-detached.
if command -v kei-memory >/dev/null 2>&1 && [ -f "$dest" ]; then
kei-memory ingest \
(
kei_with_timeout 90 kei-memory ingest \
--session-id "$session_id" \
--transcript "$dest" \
>/dev/null 2>&1 || true
>>"$bg_log" 2>&1 \
|| printf '[%s] kei-memory ingest timeout/fail for %s\n' \
"$(date +%H:%M:%S)" "$session_id" >>"$bg_log"
) </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi
# Wave 25 — frustration-matrix scan: regex+firmware classifier produces a
# JSONL of per-line affect hits per session, much smaller than the full
# transcript. Cloud REM agent reads the affect file instead of 80MB JSONL.
# Silent no-op when the primitive is absent.
# Wave 25 — frustration-matrix scan.
if command -v frustration-matrix >/dev/null 2>&1; then
affect_dir="${HOME}/.claude/memory/affect"
mkdir -p "$affect_dir" 2>/dev/null || true
affect_out="${affect_dir}/${session_id}.jsonl"
frustration-matrix scan \
(
kei_with_timeout 60 frustration-matrix scan \
--root "$traces_dir" \
--since 1d \
--format jsonl \
--output "$affect_out" \
>/dev/null 2>&1 || true
>>"$bg_log" 2>&1 || true
) </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi
# v0.11 sleep-sync (RULE 0.15) — push traces to the user's memory-repo so a
# cloud agent can consolidate them overnight. Silent no-op when the primitive
# is absent or the user hasn't opted in via /sleep-setup.
# v0.11 sleep-sync (RULE 0.15) — push traces to memory-repo.
sleep_sync="${HOME}/.claude/agents/_primitives/kei-sleep-sync.sh"
if [ -x "$sleep_sync" ]; then
"$sleep_sync" >/dev/null 2>&1 || true
(
kei_with_timeout 120 "$sleep_sync" >>"$bg_log" 2>&1 || true
) </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi
exit 0

View file

@ -1,33 +1,42 @@
#!/usr/bin/env bash
# kei-agent-cli — invoke a KeiSeiKit agent via an external LLM CLI backend.
#
# Usage:
# kei run-via <backend> <agent-name> "<task>" # by agent name
# kei run-via <backend> --file=<path> "<task>" # by agent file path
# kei run-via list # show backends + status
# Two entry points (both route through this script):
#
# kei run-via <backend> <agent> "<task>" # explicit backend
# kei agent <agent> "<task>" # backend resolved from DNA:
# # 1. --on=<backend> flag
# # 2. agent manifest's `provider`
# # 3. ~/.claude/config/primary.toml
# # 4. fallback: claude
#
# Other forms:
# kei run-via list # show backends + agents
# kei agent --on=<backend> <agent> "<task>" # override DNA backend
# kei primary # print current primary
# kei primary <backend> # set primary provider
# kei run-via --help
#
# Backends (SSoT: _primitives/cli-backends.toml, fallback table below):
# claude Claude Code (claude -p)
# grok xAI Grok (grok --print, native --agent supported)
# agy Antigravity (agy --print) — alias: antigravity
# copilot GitHub Copilot (copilot --prompt)
# kimi Moonshot Kimi (stdin, TUI primary)
# codex OpenAI Codex (codex -p) — register-only if not installed
# Backends (SSoT: _primitives/cli-backends.toml):
# claude grok agy copilot kimi codex
#
# Reads agent prompt from ~/.claude/agents/<agent-name>.md (assembler output).
# Reads assembled prompt from ~/.claude/agents/<agent-name>.md.
# Strips YAML frontmatter, composes with task, execs the CLI.
#
# Env overrides:
# KEI_AGENTS_DIR agent .md dir (default: ~/.claude/agents)
# KEI_MANIFESTS_DIR manifest .toml dir (default: ~/.claude/_manifests)
# KEI_PRIMARY override primary backend (beats config file)
# KEI_NATIVE_AGENT=1 prefer backend's native --agent flag (grok/claude)
set -euo pipefail
KEI_AGENTS_DIR="${KEI_AGENTS_DIR:-$HOME/.claude/agents}"
KEI_MANIFESTS_DIR="${KEI_MANIFESTS_DIR:-$HOME/.claude/_manifests}"
KEI_PRIMARY_CFG="${KEI_PRIMARY_CFG:-$HOME/.claude/config/primary.toml}"
KEI_NATIVE_AGENT="${KEI_NATIVE_AGENT:-0}"
usage() { sed -n '2,20p' "$0" | sed 's|^# \{0,1\}||'; }
usage() { sed -n '2,32p' "$0" | sed 's|^# \{0,1\}||'; }
# ---- backend table (SSoT mirror; kept in sync with cli-backends.toml) -----
backend_bin() {
@ -46,9 +55,49 @@ backend_supports_native_agent() {
case "$1" in claude|grok) return 0 ;; *) return 1 ;; esac
}
# Invoke backend with composed prompt as argument or stdin per backend.
# ---- DNA resolver: agent → preferred backend --------------------------------
# Reads `provider = "..."` line from the manifest TOML if present.
manifest_provider() {
local agent="$1" tomlf="$KEI_MANIFESTS_DIR/$1.toml"
[ -f "$tomlf" ] || return 1
awk -F'=' '
/^provider[[:space:]]*=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
gsub(/^"|"$/, "", $2)
print $2; exit
}
' "$tomlf"
}
# Reads primary from config file (or KEI_PRIMARY env override).
config_primary() {
if [ -n "${KEI_PRIMARY:-}" ]; then
printf '%s\n' "$KEI_PRIMARY"; return 0
fi
[ -f "$KEI_PRIMARY_CFG" ] || return 1
awk -F'=' '
/^provider[[:space:]]*=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
gsub(/^"|"$/, "", $2)
print $2; exit
}
' "$KEI_PRIMARY_CFG"
}
# Resolution order: explicit --on= → manifest provider → primary → claude.
resolve_backend() {
local agent="$1" explicit="${2:-}" out=""
if [ -n "$explicit" ]; then printf '%s\n' "$explicit"; return 0; fi
out=$(manifest_provider "$agent" 2>/dev/null) || true
if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi
out=$(config_primary 2>/dev/null) || true
if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi
printf 'claude\n'
}
# ---- backend invocation ---------------------------------------------------
backend_invoke() {
local backend="$1" prompt="$2" bin agent_name="${3:-}"
local backend="$1" prompt="$2" agent_name="${3:-}" bin
bin=$(backend_bin "$backend") || {
printf '[kei-agent-cli] unknown backend: %s\n' "$backend" >&2
return 2
@ -70,9 +119,20 @@ backend_invoke() {
claude) exec "$bin" -p "$prompt" ;;
grok|agy|antigravity) exec "$bin" --print "$prompt" ;;
copilot) exec "$bin" --prompt "$prompt" ;;
kimi) # Kimi non-interactive surface is limited;
# stdin works against TUI default mode.
printf '%s\n' "$prompt" | exec "$bin" ;;
kimi)
# Kimi has NO one-shot print mode (smoke-tested 2026-05-26): bare `kimi`
# opens an interactive TUI that ignores piped stdin and exits with "Bye!".
# For headless invocation we'd need an ACP client (`kimi acp` is a JSON-RPC
# stdio server). Until KeiSeiKit ships that client, dump the composed
# prompt to a tmpfile and open the TUI so the user can paste it in.
tmp=$(mktemp -t kei-agent-kimi.XXXX.md)
printf '%s\n' "$prompt" > "$tmp"
printf '[kei-agent-cli] kimi non-interactive is unsupported (TUI only).\n' >&2
printf '[kei-agent-cli] composed prompt saved: %s\n' "$tmp" >&2
printf '[kei-agent-cli] copy-paste it into Kimi after the TUI opens.\n' >&2
printf '[kei-agent-cli] (or pipe via `kimi acp` if you have an ACP client.)\n' >&2
exec "$bin"
;;
codex) exec "$bin" -p "$prompt" ;;
esac
}
@ -80,7 +140,6 @@ backend_invoke() {
# ---- agent loader -------------------------------------------------------
load_agent() {
local name="$1" path
# explicit path via --file=
case "$name" in
--file=*) path="${name#--file=}" ;;
/*|./*|*/*) path="$name" ;;
@ -96,16 +155,35 @@ load_agent() {
fi
return 1
fi
# strip YAML frontmatter (---\n...\n---) if present
awk '
BEGIN { in_fm=0; past_fm=0 }
BEGIN { in_fm=0 }
NR==1 && /^---$/ { in_fm=1; next }
in_fm && /^---$/ { in_fm=0; past_fm=1; next }
in_fm && /^---$/ { in_fm=0; next }
in_fm { next }
{ print }
' "$path"
}
# ---- primary subcommand ------------------------------------------------
handle_primary() {
local arg="${1:-}"
if [ -z "$arg" ]; then
cur=$(config_primary 2>/dev/null || true)
printf 'primary provider: %s\n' "${cur:-claude (default fallback)}"
[ -f "$KEI_PRIMARY_CFG" ] && printf 'config: %s\n' "$KEI_PRIMARY_CFG"
return 0
fi
backend_bin "$arg" >/dev/null || {
printf '[kei-primary] unknown backend: %s\n' "$arg" >&2
printf 'valid: claude grok agy copilot kimi codex\n' >&2
return 2
}
mkdir -p "$(dirname "$KEI_PRIMARY_CFG")"
printf '# kei primary — written %s\nprovider = "%s"\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$arg" > "$KEI_PRIMARY_CFG"
printf 'primary provider set: %s → %s\n' "$arg" "$KEI_PRIMARY_CFG"
}
# ---- subcommands --------------------------------------------------------
case "${1:-}" in
""|-h|--help|help) usage; exit 0 ;;
@ -119,6 +197,8 @@ case "${1:-}" in
printf ' %-10s ✗ (not on PATH)\n' "$b"
fi
done
cur=$(config_primary 2>/dev/null || true)
printf '\nprimary: %s\n' "${cur:-claude (default)}"
printf '\nAgents (%s):\n' "$KEI_AGENTS_DIR"
if [ -d "$KEI_AGENTS_DIR" ]; then
find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \
@ -128,26 +208,62 @@ case "${1:-}" in
fi
exit 0
;;
primary)
shift
handle_primary "${1:-}"
exit $?
;;
agent)
# Direct-invocation passthrough: `kei-agent-cli.sh agent <name> "task"`
# behaves identically to `kei-agent-cli.sh <name> "task"` (DNA mode).
# Lets users call either form without surprise.
shift
;;
esac
# ---- main ---------------------------------------------------------------
if [ $# -lt 3 ]; then
# ---- main: DNA mode (no leading backend) OR explicit run-via ------------
# Detect call shape:
# "$1" is a known backend → run-via flow (kei run-via <backend> <agent> "task")
# "$1" starts with --on= → DNA flow with override
# "$1" is anything else → DNA flow (kei agent <agent> "task")
EXPLICIT_BACKEND=""
case "${1:-}" in
--on=*)
EXPLICIT_BACKEND="${1#--on=}"
shift
;;
*)
if [ $# -ge 1 ] && backend_bin "$1" >/dev/null 2>&1; then
EXPLICIT_BACKEND="$1"
shift
fi
;;
esac
if [ $# -lt 2 ]; then
usage
exit 2
fi
BACKEND="$1"; AGENT_REF="$2"; shift 2
AGENT_REF="$1"; shift
TASK="$*"
AGENT_NAME=$(basename "${AGENT_REF#--file=}")
AGENT_NAME="${AGENT_NAME%.md}"
BACKEND=$(resolve_backend "$AGENT_NAME" "$EXPLICIT_BACKEND")
if ! AGENT_PROMPT=$(load_agent "$AGENT_REF"); then
exit 1
fi
# Compose: agent system + task delimiter.
COMPOSED=$(printf '%s\n\n---\n\nTASK FOR THIS RUN:\n%s\n' "$AGENT_PROMPT" "$TASK")
# Derive a clean agent name for KEI_NATIVE_AGENT path.
AGENT_NAME=$(basename "${AGENT_REF#--file=}")
AGENT_NAME="${AGENT_NAME%.md}"
printf '[kei-agent-cli] agent=%s backend=%s (via %s)\n' \
"$AGENT_NAME" "$BACKEND" \
"$([ -n "$EXPLICIT_BACKEND" ] && echo explicit \
|| ([ -n "$(manifest_provider "$AGENT_NAME" 2>/dev/null)" ] && echo manifest \
|| ([ -n "$(config_primary 2>/dev/null)" ] && echo primary || echo default)))" >&2
backend_invoke "$BACKEND" "$COMPOSED" "$AGENT_NAME"