feat(dna): provider+model agent DNA; kei primary; smoke 4/5 + RULE 0.14 hang fix (#46)

Mirror of keigit e4980f6a. Multi-LLM DNA + Recombobulating async fix.
This commit is contained in:
KeiSei84 2026-05-26 15:22:41 +07:00 committed by GitHub
parent ef7e695227
commit a6a540a45f
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!("description: {}\n", desc.trim()));
out.push_str(&format!("tools: {}\n", m.tools.join(", "))); out.push_str(&format!("tools: {}\n", m.tools.join(", ")));
out.push_str(&format!("model: {}\n", m.model)); 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("---\n\n");
out.push_str(&format!( out.push_str(&format!(
"<!-- GENERATED by _assembler (Rust) from _manifests/{}.toml — DO NOT EDIT. Edit the manifest. -->\n\n", "<!-- 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 description: String,
pub tools: Vec<String>, pub tools: Vec<String>,
pub model: 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 role: String,
pub blocks: Vec<String>, pub blocks: Vec<String>,
/// v0.16 (phase 5): agent substrate role. When present, assembler loads /// 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] [backend.kimi]
bin = "kimi" bin = "kimi"
prompt_flag = "stdin" prompt_flag = "tui-only"
notes = "Moonshot Kimi CLI — primarily TUI/ACP; non-interactive via stdin" 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/" homepage = "https://moonshotai.github.io/kimi-cli/"
[backend.codex] [backend.codex]

26
bin/kei
View file

@ -10,10 +10,12 @@
# 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 run-via <backend> <agent> "<task>" # kei agent <name> "<task>" # invoke agent, backend from DNA → primary
# # invoke a KeiSeiKit agent via an external LLM CLI # 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 # # 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) # kei [args...] # splash → claude args... (forwarded verbatim)
# #
# The splash shows: substrate version, agent count, last sleep run, # The splash shows: substrate version, agent count, last sleep run,
@ -24,8 +26,12 @@
set -e set -e
# --- subcommand dispatch (before splash) --------------------------------- # --- subcommand dispatch (before splash) ---------------------------------
# `kei message ...` → mailbox CLI; `kei configure` → hook/stack re-picker; # `kei message ...` → mailbox CLI
# `kei run-via ...` → invoke agent through external LLM CLI; rest = launch. # `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 case "${1:-}" in
message|msg|m) message|msg|m)
shift shift
@ -35,10 +41,18 @@ case "${1:-}" in
shift shift
exec "$HOME/.claude/scripts/kei-configure.sh" "$@" exec "$HOME/.claude/scripts/kei-configure.sh" "$@"
;; ;;
run-via|via|run|agent-via) agent)
shift shift
exec "$HOME/.claude/scripts/kei-agent-cli.sh" "$@" 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 esac
# --- args ---------------------------------------------------------------- # --- args ----------------------------------------------------------------

View file

@ -1,53 +1,99 @@
# Multi-CLI agent invocation # Multi-CLI agent invocation
> *Cross-LLM agent execution. Same agent definition, different backend.* > *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 KeiSeiKit agents are markdown files. Any LLM CLI that takes a prompt can
host them — `kei run-via` is the launcher that bridges them. host them. Three call shapes:
## 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
```bash ```bash
# Invoke the 'critic' agent through Grok with a task: kei agent <name> "<task>" # DNA-resolved (manifest → primary → claude)
kei run-via grok critic "review src/auth.rs for variant analysis" kei agent --on=<backend> <name> "<task>" # override DNA
kei run-via <backend> <name> "<task>" # explicit backend (no DNA lookup)
```
# Same agent, different backend: ## Backends — smoke-tested 2026-05-26
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"
# Point at an arbitrary agent .md (not in ~/.claude/agents/): | Backend | CLI | Flag | Smoke | Notes |
kei run-via grok --file=/tmp/my-agent.md "do the thing" |----------|-----------|--------------|-------|-------|
| 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): Run `kei run-via list` to see installed backends, current primary, and agent names.
KEI_NATIVE_AGENT=1 kei run-via grok critic "review src/auth.rs"
## 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 ## How it works
1. Reads `~/.claude/agents/<agent-name>.md` (assembler-generated prompt). 1. Resolves backend from DNA (see above).
2. Strips YAML frontmatter. 2. Reads `~/.claude/agents/<agent-name>.md` (assembler-generated prompt).
3. Composes with task as: `<agent prompt>\n\n---\n\nTASK FOR THIS RUN:\n<task>`. 3. Strips YAML frontmatter.
4. Execs the backend's non-interactive CLI with the composed prompt. 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). authentication is whatever each CLI uses (its own login / config dir).
## When to use each ## 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 - **Independent second opinion** — same agent, different model, see if
conclusions diverge. 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 ## Adding a new backend
1. Add a `[backend.<name>]` table to `_primitives/cli-backends.toml`. 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 2. Add a case arm in `scripts/kei-agent-cli.sh` `backend_bin()` and
`backend_invoke()` for the new CLI's print-flag. `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 ## 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 - Not a federation — each backend runs independently with its own
context; there is no cross-backend state. 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 - 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 do (Bash, file edits, MCP, etc.) is determined by that CLI, not
KeiSeiKit. The substrate only ships the prompt. 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 cp -f "$transcript" "$dest" 2>/dev/null || true
fi 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 if command -v kei-memory >/dev/null 2>&1 && [ -f "$dest" ]; then
kei-memory ingest \ (
--session-id "$session_id" \ kei_with_timeout 90 kei-memory ingest \
--transcript "$dest" \ --session-id "$session_id" \
>/dev/null 2>&1 || true --transcript "$dest" \
>>"$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 fi
# Wave 25 — frustration-matrix scan: regex+firmware classifier produces a # Wave 25 — frustration-matrix scan.
# 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.
if command -v frustration-matrix >/dev/null 2>&1; then if command -v frustration-matrix >/dev/null 2>&1; then
affect_dir="${HOME}/.claude/memory/affect" affect_dir="${HOME}/.claude/memory/affect"
mkdir -p "$affect_dir" 2>/dev/null || true mkdir -p "$affect_dir" 2>/dev/null || true
affect_out="${affect_dir}/${session_id}.jsonl" affect_out="${affect_dir}/${session_id}.jsonl"
frustration-matrix scan \ (
--root "$traces_dir" \ kei_with_timeout 60 frustration-matrix scan \
--since 1d \ --root "$traces_dir" \
--format jsonl \ --since 1d \
--output "$affect_out" \ --format jsonl \
>/dev/null 2>&1 || true --output "$affect_out" \
>>"$bg_log" 2>&1 || true
) </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi fi
# v0.11 sleep-sync (RULE 0.15) — push traces to the user's memory-repo so a # v0.11 sleep-sync (RULE 0.15) — push traces to memory-repo.
# cloud agent can consolidate them overnight. Silent no-op when the primitive
# is absent or the user hasn't opted in via /sleep-setup.
sleep_sync="${HOME}/.claude/agents/_primitives/kei-sleep-sync.sh" sleep_sync="${HOME}/.claude/agents/_primitives/kei-sleep-sync.sh"
if [ -x "$sleep_sync" ]; then 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 fi
exit 0 exit 0

View file

@ -1,33 +1,42 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# kei-agent-cli — invoke a KeiSeiKit agent via an external LLM CLI backend. # kei-agent-cli — invoke a KeiSeiKit agent via an external LLM CLI backend.
# #
# Usage: # Two entry points (both route through this script):
# kei run-via <backend> <agent-name> "<task>" # by agent name #
# kei run-via <backend> --file=<path> "<task>" # by agent file path # kei run-via <backend> <agent> "<task>" # explicit backend
# kei run-via list # show backends + status # 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 # kei run-via --help
# #
# Backends (SSoT: _primitives/cli-backends.toml, fallback table below): # Backends (SSoT: _primitives/cli-backends.toml):
# claude Claude Code (claude -p) # claude grok agy copilot kimi codex
# 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
# #
# 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. # Strips YAML frontmatter, composes with task, execs the CLI.
# #
# Env overrides: # Env overrides:
# KEI_AGENTS_DIR agent .md dir (default: ~/.claude/agents) # 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) # KEI_NATIVE_AGENT=1 prefer backend's native --agent flag (grok/claude)
set -euo pipefail set -euo pipefail
KEI_AGENTS_DIR="${KEI_AGENTS_DIR:-$HOME/.claude/agents}" 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}" 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 table (SSoT mirror; kept in sync with cli-backends.toml) -----
backend_bin() { backend_bin() {
@ -46,9 +55,49 @@ backend_supports_native_agent() {
case "$1" in claude|grok) return 0 ;; *) return 1 ;; esac 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() { backend_invoke() {
local backend="$1" prompt="$2" bin agent_name="${3:-}" local backend="$1" prompt="$2" agent_name="${3:-}" bin
bin=$(backend_bin "$backend") || { bin=$(backend_bin "$backend") || {
printf '[kei-agent-cli] unknown backend: %s\n' "$backend" >&2 printf '[kei-agent-cli] unknown backend: %s\n' "$backend" >&2
return 2 return 2
@ -70,9 +119,20 @@ backend_invoke() {
claude) exec "$bin" -p "$prompt" ;; claude) exec "$bin" -p "$prompt" ;;
grok|agy|antigravity) exec "$bin" --print "$prompt" ;; grok|agy|antigravity) exec "$bin" --print "$prompt" ;;
copilot) exec "$bin" --prompt "$prompt" ;; copilot) exec "$bin" --prompt "$prompt" ;;
kimi) # Kimi non-interactive surface is limited; kimi)
# stdin works against TUI default mode. # Kimi has NO one-shot print mode (smoke-tested 2026-05-26): bare `kimi`
printf '%s\n' "$prompt" | exec "$bin" ;; # 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" ;; codex) exec "$bin" -p "$prompt" ;;
esac esac
} }
@ -80,7 +140,6 @@ backend_invoke() {
# ---- agent loader ------------------------------------------------------- # ---- agent loader -------------------------------------------------------
load_agent() { load_agent() {
local name="$1" path local name="$1" path
# explicit path via --file=
case "$name" in case "$name" in
--file=*) path="${name#--file=}" ;; --file=*) path="${name#--file=}" ;;
/*|./*|*/*) path="$name" ;; /*|./*|*/*) path="$name" ;;
@ -96,16 +155,35 @@ load_agent() {
fi fi
return 1 return 1
fi fi
# strip YAML frontmatter (---\n...\n---) if present
awk ' awk '
BEGIN { in_fm=0; past_fm=0 } BEGIN { in_fm=0 }
NR==1 && /^---$/ { in_fm=1; next } NR==1 && /^---$/ { in_fm=1; next }
in_fm && /^---$/ { in_fm=0; past_fm=1; next } in_fm && /^---$/ { in_fm=0; next }
in_fm { next } in_fm { next }
{ print } { print }
' "$path" ' "$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 -------------------------------------------------------- # ---- subcommands --------------------------------------------------------
case "${1:-}" in case "${1:-}" in
""|-h|--help|help) usage; exit 0 ;; ""|-h|--help|help) usage; exit 0 ;;
@ -119,6 +197,8 @@ case "${1:-}" in
printf ' %-10s ✗ (not on PATH)\n' "$b" printf ' %-10s ✗ (not on PATH)\n' "$b"
fi fi
done done
cur=$(config_primary 2>/dev/null || true)
printf '\nprimary: %s\n' "${cur:-claude (default)}"
printf '\nAgents (%s):\n' "$KEI_AGENTS_DIR" printf '\nAgents (%s):\n' "$KEI_AGENTS_DIR"
if [ -d "$KEI_AGENTS_DIR" ]; then if [ -d "$KEI_AGENTS_DIR" ]; then
find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \ find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \
@ -128,26 +208,62 @@ case "${1:-}" in
fi fi
exit 0 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 esac
# ---- main --------------------------------------------------------------- # ---- main: DNA mode (no leading backend) OR explicit run-via ------------
if [ $# -lt 3 ]; then # 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 usage
exit 2 exit 2
fi fi
BACKEND="$1"; AGENT_REF="$2"; shift 2 AGENT_REF="$1"; shift
TASK="$*" 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 if ! AGENT_PROMPT=$(load_agent "$AGENT_REF"); then
exit 1 exit 1
fi fi
# Compose: agent system + task delimiter.
COMPOSED=$(printf '%s\n\n---\n\nTASK FOR THIS RUN:\n%s\n' "$AGENT_PROMPT" "$TASK") 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. printf '[kei-agent-cli] agent=%s backend=%s (via %s)\n' \
AGENT_NAME=$(basename "${AGENT_REF#--file=}") "$AGENT_NAME" "$BACKEND" \
AGENT_NAME="${AGENT_NAME%.md}" "$([ -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" backend_invoke "$BACKEND" "$COMPOSED" "$AGENT_NAME"