From e4980f6ad7011c374900cb9aad9795cc20608b5f Mon Sep 17 00:00:00 2001 From: KeiSei84 <2206745@gmail.com> Date: Tue, 26 May 2026 16:21:11 +0800 Subject: [PATCH] feat(dna): provider+model in agent DNA; kei primary; smoke-tested 4/5 CLIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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= → manifest provider → primary.toml → claude Files: _assembler/src/manifest.rs + Option 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). --- _assembler/src/assembler.rs | 6 + _assembler/src/manifest.rs | 7 ++ _primitives/cli-backends.toml | 4 +- bin/kei | 26 +++- docs/encyclopedia/multi-cli-agents.md | 142 +++++++++++++++------ hooks/session-end-dump.sh | 69 +++++++--- scripts/kei-agent-cli.sh | 174 +++++++++++++++++++++----- 7 files changed, 335 insertions(+), 93 deletions(-) diff --git a/_assembler/src/assembler.rs b/_assembler/src/assembler.rs index dc0e72d..15e9dec 100644 --- a/_assembler/src/assembler.rs +++ b/_assembler/src/assembler.rs @@ -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!( "\n\n", diff --git a/_assembler/src/manifest.rs b/_assembler/src/manifest.rs index 325b365..e98763e 100644 --- a/_assembler/src/manifest.rs +++ b/_assembler/src/manifest.rs @@ -9,6 +9,13 @@ pub struct Manifest { pub description: String, pub tools: Vec, pub model: String, + /// v0.39 (multi-CLI): optional LLM provider this agent prefers when invoked + /// via `kei agent `. 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, pub role: String, pub blocks: Vec, /// v0.16 (phase 5): agent substrate role. When present, assembler loads diff --git a/_primitives/cli-backends.toml b/_primitives/cli-backends.toml index 742960c..527ab49 100644 --- a/_primitives/cli-backends.toml +++ b/_primitives/cli-backends.toml @@ -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] diff --git a/bin/kei b/bin/kei index 4aa3961..b587946 100755 --- a/bin/kei +++ b/bin/kei @@ -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 "" -# # invoke a KeiSeiKit agent via an external LLM CLI +# kei agent "" # invoke agent, backend from DNA → primary +# kei agent --on= "" # override backend +# kei run-via "" # 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 [] # 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 ---------------------------------------------------------------- diff --git a/docs/encyclopedia/multi-cli-agents.md b/docs/encyclopedia/multi-cli-agents.md index 3342317..508a249 100644 --- a/docs/encyclopedia/multi-cli-agents.md +++ b/docs/encyclopedia/multi-cli-agents.md @@ -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 "" # DNA-resolved (manifest → primary → claude) +kei agent --on= "" # override DNA +kei run-via "" # 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=` flag on the command line +2. `provider:` field in agent manifest +3. `~/.claude/config/primary.toml` (set via `kei primary `) +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 ` 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/.md` (assembler-generated prompt). -2. Strips YAML frontmatter. -3. Composes with task as: `\n\n---\n\nTASK FOR THIS RUN:\n`. -4. Execs the backend's non-interactive CLI with the composed prompt. +1. Resolves backend from DNA (see above). +2. Reads `~/.claude/agents/.md` (assembler-generated prompt). +3. Strips YAML frontmatter. +4. Composes with task: `\n\n---\n\nTASK FOR THIS RUN:\n`. +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.]` 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. diff --git a/hooks/session-end-dump.sh b/hooks/session-end-dump.sh index 7cbe8b6..02abe94 100755 --- a/hooks/session-end-dump.sh +++ b/hooks/session-end-dump.sh @@ -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 \ - --session-id "$session_id" \ - --transcript "$dest" \ - >/dev/null 2>&1 || true + ( + kei_with_timeout 90 kei-memory ingest \ + --session-id "$session_id" \ + --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 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 \ - --root "$traces_dir" \ - --since 1d \ - --format jsonl \ - --output "$affect_out" \ - >/dev/null 2>&1 || true + ( + kei_with_timeout 60 frustration-matrix scan \ + --root "$traces_dir" \ + --since 1d \ + --format jsonl \ + --output "$affect_out" \ + >>"$bg_log" 2>&1 || true + ) /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 2>&1 & + disown 2>/dev/null || true fi exit 0 diff --git a/scripts/kei-agent-cli.sh b/scripts/kei-agent-cli.sh index 502dc8a..b63b9ea 100755 --- a/scripts/kei-agent-cli.sh +++ b/scripts/kei-agent-cli.sh @@ -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 "" # by agent name -# kei run-via --file= "" # by agent file path -# kei run-via list # show backends + status +# Two entry points (both route through this script): +# +# kei run-via "" # explicit backend +# kei agent "" # backend resolved from DNA: +# # 1. --on= 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= "" # override DNA backend +# kei primary # print current primary +# kei primary # 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/.md (assembler output). +# Reads assembled prompt from ~/.claude/agents/.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 "task"` + # behaves identically to `kei-agent-cli.sh "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 "task") +# "$1" starts with --on= → DNA flow with override +# "$1" is anything else → DNA flow (kei 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"