From 49aa72f5d46349d052f29412c0523f800798ddc7 Mon Sep 17 00:00:00 2001 From: Robert Gering Date: Sun, 2 Aug 2026 23:28:52 +0200 Subject: [PATCH 1/5] Add kimi CLI as a kickoff worker: registry entry + launch argv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kimi-code has no positional launch prompt, no initial-prompt env var, and piped stdin only prefills the input box without submitting (and would steal the TUI's tty). `-p` is the only way in, but it is mutually exclusive with both --auto and -y and exits after one answer. It does run tools unattended though, and `kimi -c` inherits its full session history — so the worker launches in two phases: `-p` seeds and works the task one-shot, then `exec kimi -c --auto` takes over as the interactive autonomous session with that history. All probed live against kimi-code 0.31.1. - Registry: --kimi -> kimi:kimi-code/k3-256k (commit,pr). The model id must be the QUALIFIED alias; the bare name aborts at startup like an unknown model. - Readiness is model-aware (grok precedent) via `kimi provider list --json`, bounded. Auth probes credentials/, not the same-named oauth/ dir — that file stays 0 bytes even when logged in. `kimi doctor` only validates config syntax. - A well-formed listing offering no models means unavailable; a document without the `models` section is drift and falls back to trusting auth. - Values ride as "$1"/"$2" positionals, never spliced into the script text: `-p` swallows the next token, so a concatenated argv is one reordering away from silently eating a flag. - Tests assert the exact word list plus that structure, and execute the resolved argv against a logging stub to check what each phase received. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SkXCG4uaXDEyf3ciKYKu2i --- plugins/work-system/scripts/agent-registry.sh | 78 ++++++++- .../scripts/test_agent_registry.py | 160 +++++++++++++++++- 2 files changed, 224 insertions(+), 14 deletions(-) diff --git a/plugins/work-system/scripts/agent-registry.sh b/plugins/work-system/scripts/agent-registry.sh index 87447f5..e43f18e 100755 --- a/plugins/work-system/scripts/agent-registry.sh +++ b/plugins/work-system/scripts/agent-registry.sh @@ -12,7 +12,7 @@ # resolve [--session ] # Map a selector to launch argv + metadata. # Selectors: a shorthand flag (--fable, --opus, -# --codex, --sol, --grok), a +# --codex, --sol, --grok, --kimi), a # canonical name (claude:opus), a bare CLI # (codex -> that CLI's default model), or # cli:model (the --agent escape hatch). @@ -31,11 +31,27 @@ # the work-system continue skill resumes TASK.md deterministically) # codex -> codex -m # grok -> grok -m -# The bootstrap prompt (codex/grok have no work-system skills) tells the agent -# to read TASK.md and drive the task to a PR. `supports=` metadata records +# kimi -> sh -c 'kimi -m "$1" -p "$2"; exec kimi -c --auto' \ +# kimi-worker (seed+continue) +# The bootstrap prompt (codex/grok/kimi have no work-system skills) tells the +# agent to read TASK.md and drive the task to a PR. `supports=` metadata records # which lifecycle hooks each agent honors, so /close and /continue can degrade # for non-claude workers instead of faking claude-only behavior. # +# Why kimi needs the two-phase seed+continue shape (all probed live, 0.31.1): +# kimi has NO positional launch prompt (`kimi "text"` -> "unknown command"), no +# initial-prompt env var, and piped stdin only prefills the input box without +# submitting (and would steal the TUI's tty anyway). `-p` is the only way in, +# but it is mutually exclusive with BOTH `--auto` and `-y` and exits after one +# answer — so `-p` alone cannot be a worker. It does run tools unattended, and +# `kimi -c` inherits its full session history, so: phase 1 seeds+works the task +# one-shot, phase 2 `exec`s into the interactive autonomous session with that +# history. The `exec` matters — it re-roots the herdr pane at kimi, and `;` (not +# `&&`) keeps phase 2 alive if the seed fails, leaving a usable tab instead of a +# dead one. Values travel as "$1"/"$2" positionals, never interpolated into the +# script text: `-p` swallows the next token as its value, so an argv built by +# concatenation is one reordering away from silently eating a flag. +# # State & config (override for tests / relocation): # WORK_SYSTEM_AGENT_PROJECT_STATE the repo's default-agent file # default: /.claude/work-system-agent @@ -67,6 +83,10 @@ if [ -z "$PROJECT_STATE" ]; then [ -n "$_repo_root" ] && PROJECT_STATE="$_repo_root/.claude/work-system-agent" fi GROK_AUTH_FILE="${GROK_AUTH_FILE:-$HOME/.grok/auth.json}" +# kimi's OAuth tokens live in credentials/, NOT in the same-named oauth/ dir — +# `~/.kimi-code/oauth/kimi-code` exists but stays 0 bytes even when logged in, so +# probing that path would report every authenticated install as logged out. +KIMI_CREDENTIALS_FILE="${KIMI_CREDENTIALS_FILE:-$HOME/.kimi-code/credentials/kimi-code.json}" # The bootstrap prompt for CLIs without work-system skills (codex, grok). One # argv word; the launch helper passes it verbatim. @@ -90,7 +110,8 @@ REGISTRY='--fable|claude|fable|continue,close-exit,statusline,commit,pr -|claude|sonnet|continue,close-exit,statusline,commit,pr --codex|codex|gpt-5.6-terra|commit,pr --sol|codex|gpt-5.6-sol|commit,pr ---grok|grok|grok-4.5|commit,pr' +--grok|grok|grok-4.5|commit,pr +--kimi|kimi|kimi-code/k3-256k|commit,pr' usage() { # Usage = header comment from line 2 up to (not including) the registry @@ -157,7 +178,16 @@ row_for_selector() { # rejects an unlisted `-m` id at launch ("unknown model id"), so a # per-CLI auth check alone would mislabel a model the CLI no longer # offers (grok drops/renames models between releases) as available. -# This is the one CLI with a usable model-list command. +# kimi: install + auth file + the model must appear in `kimi provider list +# --json`. Model-aware for the same reason as grok, and the failure is +# even sharper: an unconfigured `-m` id aborts at startup +# ("Model ... is not configured in config.toml"), so a bad model would +# give the user a worker tab that dies on sight. The model id must be +# the QUALIFIED alias (`kimi-code/k3-256k`) — the bare model name is +# rejected the same way. `kimi doctor` is NOT an auth check (it only +# validates config file syntax), hence the credentials-file probe. +# The listing is local config (~0.7s, no network), but it is bounded +# anyway: a catalog refresh on start can make it reach out. # run_bounded — run cmd with a hard time bound so an external # probe can never hang `list`/the picker. Prints cmd's stdout; returns cmd's exit @@ -212,6 +242,14 @@ grok_models_raw() { run_bounded 10 grok models 2>/dev/null } +# Same contract as grok_models_raw, for kimi: RAW `kimi provider list --json` on +# stdout, exit code = fetch status. entry_status substring-matches the qualified +# model alias against the raw JSON rather than parsing it — no jq/python +# dependency, and a reshaped config document can't yield a wrong token. +kimi_models_raw() { + run_bounded 10 kimi provider list --json 2>/dev/null +} + entry_status() { local cli="$1" model="$2" avail=no note="" case "$cli" in @@ -252,6 +290,26 @@ entry_status() { else note="model not offered by this grok CLI (see: grok models)"; fi fi ;; + kimi) + if ! command -v kimi >/dev/null 2>&1; then note="not installed" + elif [ ! -s "$KIMI_CREDENTIALS_FILE" ]; then note="run: kimi login" + else + local _kraw krc=0 + _kraw="$(kimi_models_raw)" || krc=$? # exit code = fetch status + if [ "$krc" -ne 0 ]; then + # unreachable/timed out — inconclusive, trust auth (mirrors grok). + avail=yes; note="kimi provider list unreachable — availability assumed" + elif [ -z "$_kraw" ] || ! grep -qF -- '"models"' <<<"$_kraw"; then + # Empty, or a document without the `models` section we key off. Unlike + # grok's plain-text listing, a JSON reply is only self-describing while + # the schema holds: `{"models": {}}` IS a real "no models" answer, but a + # renamed/moved section is drift and must not read as one. Gate on the + # section's presence, so only the former reaches the match below. + avail=yes; note="kimi provider list unrecognized — availability assumed" + elif grep -qF -- "\"$model\"" <<<"$_kraw"; then avail=yes + else note="model not offered by this kimi CLI (see: kimi provider list)"; fi + fi + ;; *) note="unknown cli" ;; esac printf '%s\t%s\n' "$avail" "$note" @@ -276,6 +334,14 @@ emit_argv() { grok) printf 'argv=%s\n' grok -m "$model" "$BOOTSTRAP_PROMPT" ;; + kimi) + # Two-phase seed+continue (see the launch-shape note in the header). The + # model and the prompt are passed as "$1"/"$2" positionals — NOT spliced + # into the script text — so no amount of prompt content can reorder the + # flags or be absorbed by `-p`. + printf 'argv=%s\n' sh -c 'kimi -m "$1" -p "$2"; exec kimi -c --auto' \ + kimi-worker "$model" "$BOOTSTRAP_PROMPT" + ;; esac } @@ -303,7 +369,7 @@ subcmd_resolve() { local record record="$(row_for_selector "$selector")" || { echo "Unknown agent selector: $selector" >&2 - echo "Try: --fable --opus --codex --sol --grok, a name (claude:opus), or a cli (codex)" >&2 + echo "Try: --fable --opus --codex --sol --grok --kimi, a name (claude:opus), or a cli (codex)" >&2 exit 2 } local flag cli model supports diff --git a/plugins/work-system/scripts/test_agent_registry.py b/plugins/work-system/scripts/test_agent_registry.py index 9a49d65..2b555fb 100644 --- a/plugins/work-system/scripts/test_agent_registry.py +++ b/plugins/work-system/scripts/test_agent_registry.py @@ -3,13 +3,17 @@ or via scripts/check-structure.py's "plugin tests" check. Guards the registry's contract: alias/name/cli selector resolution, the per-CLI -launch argv shape (claude `/work-system:continue` vs codex/grok bootstrap prompt), the -availability probe (codex login status + grok auth file + grok model-list), the -exit-code map (2 unknown selector, 3 resolved-but-unavailable), and the -project-default state (set/get, bogus rejection, no-git-repo error). - -Availability is made deterministic with fake `codex`/`grok`/`claude` stubs on a -prepended PATH, so the test does not depend on what is really installed/authed. +launch argv shape (claude `/work-system:continue` vs the codex/grok/kimi bootstrap +prompt, incl. kimi's two-phase seed+continue argv and its argument-order +regression), the availability probe (codex login status + grok/kimi auth file + +grok/kimi model-list), the exit-code map (2 unknown selector, 3 +resolved-but-unavailable), and the project-default state (set/get, bogus +rejection, no-git-repo error). + +Availability is made deterministic with fake `codex`/`grok`/`kimi`/`claude` stubs +on a prepended PATH, so the test does not depend on what is really +installed/authed. The kimi stub also logs every invocation's argv, so the +resolved launch argv can be executed for real and each phase asserted. """ import json import os @@ -33,7 +37,9 @@ class Env: """A throwaway HOME + fake-bin sandbox controlling CLI availability.""" def __init__(self, codex_authed=True, grok_authed=True, - grok_models=("grok-4.5",), grok_models_ok=True): + grok_models=("grok-4.5",), grok_models_ok=True, + kimi_authed=True, kimi_models=("kimi-code/k3-256k",), + kimi_models_ok=True, kimi_schema_ok=True): self.tmp = tempfile.TemporaryDirectory() root = Path(self.tmp.name) self.home = root / "home" @@ -64,18 +70,43 @@ def __init__(self, codex_authed=True, grok_authed=True, ) # claude stub: only ever hit by `command -v`. (bindir / "claude").write_text("#!/bin/sh\nexit 0\n") + # kimi stub: `provider list --json` drives the model-level probe (raw + # JSON, substring-matched). Every invocation also appends its full argv + # (one tab-joined line per call) to KIMI_ARGLOG, so a test can execute + # the resolved launch argv for real and assert what each phase received. + self.kimi_arglog = root / "kimi_args.log" + # kimi_schema_ok=False simulates format drift: a valid but differently + # shaped document (no `models` section) — distinct from a well-formed + # listing that genuinely offers nothing. + kimi_section = "models" if kimi_schema_ok else "aliases" + kimi_model_json = ",".join('\\"%s\\": {}' % m for m in kimi_models) + (bindir / "kimi").write_text( + "#!/bin/sh\n" + '[ -n "$KIMI_ARGLOG" ] && { printf \'%%s\\t\' "$@" >> "$KIMI_ARGLOG"; ' + 'printf \'\\n\' >> "$KIMI_ARGLOG"; }\n' + 'if [ "$1" = "provider" ] && [ "$2" = "list" ]; then\n' + ' echo "{\\"%s\\": {%s}}"\n' + " exit %d\n" + "fi\n" + "exit 0\n" % (kimi_section, kimi_model_json, 0 if kimi_models_ok else 1) + ) for f in bindir.iterdir(): f.chmod(0o755) # grok auth file toggles grok readiness. self.grok_auth = root / "grok_auth.json" if grok_authed: self.grok_auth.write_text("{}\n") + # kimi's real tokens live in credentials/, not the same-named oauth/ dir. + self.kimi_creds = root / "kimi_credentials.json" + if kimi_authed: + self.kimi_creds.write_text("{}\n") self.project_state = root / "repo" / ".claude" / "work-system-agent" self.env = dict(os.environ) self.env["PATH"] = f"{bindir}:{self.env['PATH']}" self.env["HOME"] = str(self.home) self.env["GROK_AUTH_FILE"] = str(self.grok_auth) + self.env["KIMI_CREDENTIALS_FILE"] = str(self.kimi_creds) self.env["WORK_SYSTEM_AGENT_PROJECT_STATE"] = str(self.project_state) def run(self, *args, project_state=True): @@ -89,6 +120,17 @@ def run(self, *args, project_state=True): env=env, cwd=str(self.home), capture_output=True, text=True, ) + def run_argv(self, argv): + """Execute a resolved launch argv against the stubs; return the per-call + argv lines the kimi stub recorded.""" + env = dict(self.env) + env["KIMI_ARGLOG"] = str(self.kimi_arglog) + self.kimi_arglog.write_text("") + subprocess.run(argv, env=env, cwd=str(self.home), + capture_output=True, text=True, timeout=30) + return [line.split("\t")[:-1] + for line in self.kimi_arglog.read_text().splitlines()] + def close(self): self.tmp.cleanup() @@ -134,6 +176,56 @@ def kv(out): check("--grok -> grok:grok-4.5", r.get("name") == "grok:grok-4.5") check("grok argv shape", r["argv"][:3] == ["grok", "-m", "grok-4.5"]) +# --- kimi: the two-phase seed+continue launch argv ------------------------- # +# kimi has no positional launch prompt and `-p` cannot be combined with --auto/-y, +# so a worker is `-p` (seed, runs tools unattended) then `exec kimi -c --auto` +# (interactive + autonomous, inheriting the seed's session history). +KIMI_SCRIPT = 'kimi -m "$1" -p "$2"; exec kimi -c --auto' + +r = kv(e.run("resolve", "--kimi").stdout) +check("--kimi -> kimi:kimi-code/k3-256k", r.get("name") == "kimi:kimi-code/k3-256k") +check("kimi model is the QUALIFIED alias (bare name aborts at startup)", + r.get("model") == "kimi-code/k3-256k") +check("kimi supports commit,pr only", r.get("supports") == "commit,pr") +# Exact word list — the whole point of this test. +check("kimi argv shape", + r["argv"][:5] == ["sh", "-c", KIMI_SCRIPT, "kimi-worker", "kimi-code/k3-256k"]) +check("kimi argv is exactly 6 words", len(r["argv"]) == 6) +check("kimi bootstrap is the last word and mentions TASK.md", + "TASK.md" in r["argv"][5]) + +# Argument-order regression: `-p ` consumes the NEXT token, so an argv +# built by concatenation can silently swallow a flag (`kimi -p --auto "…"` -> +# --auto becomes the prompt and the prompt becomes an unknown subcommand). Two +# structural guarantees prevent that, and both are asserted: +# 1. the model and the prompt are passed as positionals, never spliced into +# the script text (so their content cannot reorder anything), and +# 2. --auto lives in a different command than -p (after the `;`), so it can +# never land in -p's value position. +script = r["argv"][2] +check("model is not spliced into the script text", "kimi-code/k3-256k" not in script) +check("prompt is not spliced into the script text", "TASK.md" not in script) +check("-p takes the positional as its value", '-p "$2"' in script) +check("--auto is in a separate command from -p", + "--auto" in script.split(";", 1)[1] and "--auto" not in script.split(";", 1)[0]) +check("no bare -p/--prompt argv word (it stays bound inside the script)", + "-p" not in r["argv"] and "--prompt" not in r["argv"]) + +# Execute the resolved argv for real against the stub and assert what each phase +# actually received — string checks alone can't prove the shell binds the values +# the way we think it does. +calls = e.run_argv(r["argv"]) +check("kimi launch runs exactly two phases", len(calls) == 2) +if len(calls) == 2: + seed, cont = calls + check("phase 1 is the -p seed with the model and prompt intact", + seed[:3] == ["-m", "kimi-code/k3-256k", "-p"] and "TASK.md" in seed[3]) + check("phase 1 got exactly 4 args (nothing swallowed, nothing extra)", + len(seed) == 4) + check("phase 1 never carries --auto/-y (mutually exclusive with -p)", + "--auto" not in seed and "-y" not in seed) + check("phase 2 is the interactive autonomous continue", cont == ["-c", "--auto"]) + # canonical name and bare-cli-default selectors check("name selector claude:sonnet", kv(e.run("resolve", "claude:sonnet").stdout).get("model") == "sonnet") @@ -205,6 +297,58 @@ def kv(out): check("empty note is soft", "assumed" in by["grok:grok-4.5"]["note"]) e.close() +# --- kimi model-level availability (same contract as grok) ----------------- # +# Model-aware for a sharper reason than grok's: an unconfigured `-m` id aborts +# kimi at startup, so a stale model would hand the user a tab that dies on sight. +e = Env(kimi_models=("kimi-code/k3-256k",)) +by = {r["name"]: r for r in json.loads(e.run("list", "--json").stdout)} +check("kimi model listed -> available", by["kimi:kimi-code/k3-256k"]["available"] is True) +check("resolve --kimi available -> exit 0", e.run("resolve", "--kimi").returncode == 0) +e.close() + +# authed, but the registry's model is not in the provider listing -> refuse now. +e = Env(kimi_models=("kimi-code/k9-imaginary",)) +by = {r["name"]: r for r in json.loads(e.run("list", "--json").stdout)} +check("kimi model not listed -> unavailable", by["kimi:kimi-code/k3-256k"]["available"] is False) +check("kimi unlisted note points at the listing", + "kimi provider list" in by["kimi:kimi-code/k3-256k"]["note"]) +check("resolve --kimi unavailable -> exit 3", e.run("resolve", "--kimi").returncode == 3) +e.close() + +# no credentials file -> logged out. (Probing ~/.kimi-code/oauth/ instead would +# report every authenticated install as logged out: that file stays 0 bytes.) +e = Env(kimi_authed=False) +by = {r["name"]: r for r in json.loads(e.run("list", "--json").stdout)} +check("kimi unauthed -> unavailable", by["kimi:kimi-code/k3-256k"]["available"] is False) +check("kimi note is login hint", "kimi login" in by["kimi:kimi-code/k3-256k"]["note"]) +e.close() + +# listing unreachable / empty-but-ok -> inconclusive, trust auth (mirrors grok). +e = Env(kimi_models=(), kimi_models_ok=False) +by = {r["name"]: r for r in json.loads(e.run("list", "--json").stdout)} +check("kimi listing unreachable -> assumed available", + by["kimi:kimi-code/k3-256k"]["available"] is True) +check("kimi unreachable note is soft", "unreachable" in by["kimi:kimi-code/k3-256k"]["note"]) +e.close() + +# A well-formed listing that offers NO models is a real answer, not drift -> +# unavailable (the launch would abort at startup anyway). +e = Env(kimi_models=(), kimi_models_ok=True) +by = {r["name"]: r for r in json.loads(e.run("list", "--json").stdout)} +check("kimi listing with zero models -> unavailable", + by["kimi:kimi-code/k3-256k"]["available"] is False) +e.close() + +# Schema drift (the `models` section renamed/moved) is NOT a real answer: the +# match would fail for a reason that says nothing about the model, so trust auth +# rather than silently disabling the whole kimi backend on a format change. +e = Env(kimi_schema_ok=False) +by = {r["name"]: r for r in json.loads(e.run("list", "--json").stdout)} +check("kimi schema drift -> assumed available", + by["kimi:kimi-code/k3-256k"]["available"] is True) +check("kimi drift note is soft", "assumed" in by["kimi:kimi-code/k3-256k"]["note"]) +e.close() + # --- project default (the only persisted state) ---------------------------- # e = Env() # nothing set -> empty (no-flag /kickoff then shows the picker) From e534812efc91c15ac8df38164b20209adc42f37c Mon Sep 17 00:00:00 2001 From: Robert Gering Date: Mon, 3 Aug 2026 11:14:09 +0200 Subject: [PATCH 2/5] Wire the kimi worker into the skills, docs and lifecycle (1.11.0) Completes the kimi worker surface started in the registry commit. - /continue: the reopen caveat now names `kimi -c` alongside `codex resume --last` / `grok -c`. Reopen still always sends `claude -c` (the worker isn't persisted per task), so the caveat stays inline. - /kickoff: --kimi in the flag list, kimi in the non-claude announce class, and the manual launch block spells out the two-phase form. Called out that shell-quoting is load-bearing there: one argv word is a script carrying `;`, `$` and quotes, so an unquoted render would run the `;` in the user's own shell and expand $1/$2 there. - Lifecycle verified rather than assumed: agent_name comes from the registry's `name=` and agent_status from herdr's pane hooks, so nothing reads argv[0] and the `sh -c` wrapper (which execs into kimi) leaves tab teardown and state detection unchanged. - Docs: both READMEs, marketplace description, CHANGELOG, and the kickoff-agent-selection knowledge entry, which records the launch-shape constraints and why grok's "empty listing = inconclusive" rule had to be re-derived for JSON. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SkXCG4uaXDEyf3ciKYKu2i --- .claude-plugin/marketplace.json | 4 +- .../features/kickoff-agent-selection.md | 50 ++++++++++++++++--- CHANGELOG.md | 7 +++ README.md | 2 +- .../work-system/.claude-plugin/plugin.json | 2 +- plugins/work-system/README.md | 33 ++++++++---- plugins/work-system/scripts/herdr-launch.sh | 4 +- plugins/work-system/skills/continue/SKILL.md | 21 ++++---- plugins/work-system/skills/kickoff/SKILL.md | 26 ++++++---- 9 files changed, 106 insertions(+), 43 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index fa0ca86..dc54678 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -17,8 +17,8 @@ { "name": "work-system", "source": "./plugins/work-system", - "description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, or grok), and track progress across the define/kickoff/continue/status/close lifecycle.", - "version": "1.10.0" + "description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, grok, or kimi), and track progress across the define/kickoff/continue/status/close lifecycle.", + "version": "1.11.0" }, { "name": "pr-flow", diff --git a/.claude/knowledge/features/kickoff-agent-selection.md b/.claude/knowledge/features/kickoff-agent-selection.md index bc80fed..05516ea 100644 --- a/.claude/knowledge/features/kickoff-agent-selection.md +++ b/.claude/knowledge/features/kickoff-agent-selection.md @@ -1,10 +1,10 @@ --- title: "Kickoff Agent Selection: registry, per-repo default, honest degradation" createdAt: 2026-07-17 -updatedAt: 2026-07-17 +updatedAt: 2026-08-03 createdFrom: "session: 2026-07-17 (task/kickoff-agent-selection)" -updatedFrom: "session: 2026-07-17" -pluginVersion: 1.9.0 +updatedFrom: "session: 2026-08-03 (task/add-kimi-worker-support)" +pluginVersion: 1.11.0 prime: false --- @@ -43,16 +43,50 @@ never hangs. A failed *or* empty-but-successful (reformatted) `grok models` is or format drift must not disable the backend. codex/claude stay auth-only (no clean model-list command). See [[swarm-backend-adapter]] for the sibling probe. +## kimi: the launch shape a CLI's flags can force on you +kimi (added 1.11.0, `--kimi` → `kimi:kimi-code/k3-256k`) is the first worker whose +argv is not ` -m `, because **no such form exists**. Probed +live on 0.31.1: no positional launch prompt (`kimi "text"` → "unknown command"), +no initial-prompt env var, and piped stdin only prefills the input box without +submitting — and in a pane it would steal the TUI's tty anyway. `-p` is the sole +entry point but is mutually exclusive with **both** `--auto` and `-y` and exits +after one answer, so it cannot *be* the worker. What makes it work: `-p` runs +tools unattended, and `kimi -c` inherits its session history. Hence two phases: + + sh -c 'kimi -m "$1" -p "$2"; exec kimi -c --auto' kimi-worker + +`exec` re-roots the pane at kimi (herdr then watches the real process); `;` not +`&&` keeps phase 2 alive if the seed fails, leaving a usable tab. Values ride as +`"$1"`/`"$2"` positionals, never spliced into the script text — `-p` swallows the +next token, so a concatenated argv is one reordering away from silently eating a +flag (`kimi -p --auto "…"` makes `--auto` the prompt). The test asserts the exact +word list *and* executes the argv against a logging stub, because string checks +can't prove the shell binds values the way you think. Two more traps: `-m` needs +the **qualified** alias (`kimi-code/k3-256k`; the bare name aborts at startup like +an unknown model — hence model-aware readiness), and auth lives in +`credentials/`, not the same-named `oauth/` dir, which stays 0 bytes when logged +in. `kimi doctor` only validates config syntax; it is not an auth check. + +**JSON breaks grok's "empty = inconclusive" rule.** grok treats empty output as +drift and trusts auth. For kimi's `provider list --json`, `{"models": {}}` is +non-empty yet a *real* "no models" answer. So the gate is the **section's +presence**: no `"models"` key → drift → trust auth; present but no match → truly +unavailable. Transplanting the sibling's rule verbatim would have mislabeled +either case. + ## Non-claude degradation: document, don't fake -codex/grok have no work-system skills, so a launched worker gets a bootstrap +codex/grok/kimi have no work-system skills, so a launched worker gets a bootstrap prompt (read TASK.md → commit → PR) instead of `/continue`. Everything git/PR-derived (`/status`, `/list`, `[ws]` statusline, `/close` tab teardown) -is CLI-agnostic. `/close` Scenario B (`/exit` self-teardown) is claude-only *by +is CLI-agnostic — `agent_name` comes from the registry's `name=`, never argv[0], +and `agent_status` from herdr's own pane hooks, so kimi's `sh -c` wrapper changes +nothing. `/close` Scenario B (`/exit` self-teardown) is claude-only *by construction* (only a claude session can invoke `/close` from inside its tab). `/continue` reopen **always sends `claude -c`** — the worker is not persisted -per task (per-task agent memory is a deliberate later idea), so for a codex/grok -task the user resumes the real worker themselves; the skill surfaces this inline -rather than pretending. `supports=` in the registry is **reserved** metadata +per task (per-task agent memory is a deliberate later idea), so for a +codex/grok/kimi task the user resumes the real worker themselves (`codex resume +--last` / `grok -c` / `kimi -c`); the skill surfaces this inline rather than +pretending. `supports=` in the registry is **reserved** metadata (the seed for the manager/worker-orchestration design) — not yet consumed. ## Security: announce, don't prompt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6e295..281fd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,13 @@ entries are grouped per plugin, newest first. ## work-system +### 1.11.0 — 2026-08-03 +- `/kickoff` can launch the **kimi CLI** (kimi-code) as a worker: `--kimi` → `kimi:kimi-code/k3-256k`, joining claude/codex/grok in `agent-registry.sh`. It appears in the picker and can be saved as the repo default, where it announces like the other third-party workers. +- kimi is the first worker without a ` -m ` launch form — it has no positional launch prompt, no initial-prompt env var, and piped stdin only prefills the input box (and would steal the TUI's tty). Its `-p` flag is the only entry point, but it cannot be combined with `--auto`/`-y` and exits after one answer. Since `-p` does run tools unattended and `kimi -c` inherits its history, the launch is two-phase: `sh -c 'kimi -m "$1" -p "$2"; exec kimi -c --auto' …` — the seed works the task through once, then `exec` hands over to the interactive autonomous session. So a kimi tab has already made progress by the time you switch to it. +- The model and prompt travel as `"$1"`/`"$2"` positionals rather than spliced into the script text: `-p` consumes the next token, so a concatenated argv can silently swallow a flag (`kimi -p --auto "…"` turns `--auto` into the prompt). Tests assert the exact word list, that structure, and — by executing the resolved argv against a logging stub — what each phase actually received. +- Readiness is model-aware via `kimi provider list --json` (bounded), because an unconfigured `-m` id aborts kimi at startup; the id must be the **qualified** alias (`kimi-code/k3-256k`). Auth probes `credentials/kimi-code.json`, not the same-named `oauth/` file, which stays 0 bytes even when logged in. A listing without the `models` section counts as format drift and falls back to trusting auth, while a well-formed listing offering nothing means unavailable — grok's "empty output = inconclusive" rule doesn't transfer to JSON. +- `/continue`'s reopen caveat and the README now name `kimi -c` alongside `codex resume --last` / `grok -c`. Lifecycle is unchanged: `agent_name` comes from the registry and `agent_status` from herdr's pane hooks, so the `sh -c` wrapper (which `exec`s into kimi) doesn't affect tab teardown or state. + ### 1.10.0 — 2026-07-24 - `/close` step 10's commit+push prompt is now skippable per repo: a committed `.claude/work-system-close-autocommit` flag (mirrors the `.claude/work-system-agent` default precedent) routes straight to `archive-task.sh commit-push` — no `AskUserQuestion` — and reports the result exactly as the manual path does. Off by default; unset repos keep today's ask-once behavior. Per-repo only, no global default. `archive-task.sh` grew an `autocommit get|set|unset` subcommand as the single source of truth for the flag. - The flag is honored **only once committed**: `get` reads the value from the committed object on the default branch (`git show refs/heads/:` — fully qualified, so a same-named tag cannot shadow the branch), never from the working tree, so a file a tool or a worktree agent merely wrote cannot waive the prompt — and neither can a working-tree edit hidden behind `git update-index --assume-unchanged`/`--skip-worktree`, which fools a diff-based guard. A locally edited flag still falls back to asking, so deliberate local disabling works. Scope stated honestly: this raises the bar from "any file write" to "a commit", not to "human-reviewed"; `commit-push`'s own guards (archive-scoped pathspec, ff-only, never force-push, refusal on unpushed history) are what bound the damage. diff --git a/README.md b/README.md index be38db5..1df7b9f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Lightweight, native knowledge management for Claude Code projects. Three layers ### Work System -Generic task and worktree workflow system. Manage tasks as markdown files, work in isolated git worktrees, and track progress through the full lifecycle. `/kickoff` runs the repo's default worker agent (Claude, codex, or grok — a single committed per-project default), or, when none is set, shows a picker and offers to save your choice; override per run with flags like `--opus`/`--sol` or `--pick`. Inside a [herdr](plugins/work-system/README.md#herdr-integration) session it auto-opens a tab (named after the task, shortened for the sidebar and prefixed with the task's state glyph — `●` active, `◇` in review, `◆` approved, `✓` merged) with the worktree as cwd, starts the chosen worker, and — for a Claude worker — runs `/work-system:continue` for you (plugin-qualified, since a Claude Code built-in `/continue` shadows the bare skill); `/adopt` auto-opens the same tab once it has built the worktree from an existing branch; `/work-system:continue ` from the main session reopens that tab and resumes it if a stray `/exit` closed it; and `/close` tears the tab down again when the task is merged. +Generic task and worktree workflow system. Manage tasks as markdown files, work in isolated git worktrees, and track progress through the full lifecycle. `/kickoff` runs the repo's default worker agent (Claude, codex, grok, or kimi — a single committed per-project default), or, when none is set, shows a picker and offers to save your choice; override per run with flags like `--opus`/`--sol` or `--pick`. Inside a [herdr](plugins/work-system/README.md#herdr-integration) session it auto-opens a tab (named after the task, shortened for the sidebar and prefixed with the task's state glyph — `●` active, `◇` in review, `◆` approved, `✓` merged) with the worktree as cwd, starts the chosen worker, and — for a Claude worker — runs `/work-system:continue` for you (plugin-qualified, since a Claude Code built-in `/continue` shadows the bare skill); `/adopt` auto-opens the same tab once it has built the worktree from an existing branch; `/work-system:continue ` from the main session reopens that tab and resumes it if a stray `/exit` closed it; and `/close` tears the tab down again when the task is merged. **Commands:** `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/list`, `/statusline` diff --git a/plugins/work-system/.claude-plugin/plugin.json b/plugins/work-system/.claude-plugin/plugin.json index c3bf4b9..05c00f4 100644 --- a/plugins/work-system/.claude-plugin/plugin.json +++ b/plugins/work-system/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "work-system", "description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, or grok), and track progress across the define/kickoff/continue/status/close lifecycle.", - "version": "1.10.0", + "version": "1.11.0", "author": { "name": "gering" }, diff --git a/plugins/work-system/README.md b/plugins/work-system/README.md index 9d24dbe..a4b22a7 100644 --- a/plugins/work-system/README.md +++ b/plugins/work-system/README.md @@ -28,7 +28,7 @@ Generic task and worktree workflow system for Claude Code. Manage tasks as markd | Command | Description | |---------|-------------| | `/define` | Create a new task (markdown file with Goal/Context/Requirements) | -| `/kickoff` | Start a task in an isolated git worktree, with a choice of worker agent (Claude/codex/grok) | +| `/kickoff` | Start a task in an isolated git worktree, with a choice of worker agent (Claude/codex/grok/kimi) | | `/adopt` | Adopt an existing branch into the work system | | `/continue` | Resume the current task (in a worktree); or `/continue ` from the main session reopens the task's herdr tab and resumes it | | `/status` | Check task status (PRs, branches, commits) | @@ -109,6 +109,7 @@ shows a picker and offers to save your choice as the default: > /kickoff add-dark-mode --opus # claude on opus > /kickoff add-dark-mode --sol # codex on gpt-5.6-sol > /kickoff add-dark-mode --grok # grok-4.5 +> /kickoff add-dark-mode --kimi # kimi-code on k3-256k > /kickoff add-dark-mode --pick # force the interactive picker ``` @@ -187,6 +188,7 @@ flag picks another: | `--fable` / `--opus` | claude on fable / opus | | `--codex` / `--sol` | codex on gpt-5.6-terra / gpt-5.6-sol | | `--grok` | grok-4.5 | +| `--kimi` | kimi-code on k3-256k (launches in two phases — see below) | | `--agent ` | any registry entry, e.g. `--agent claude:sonnet` or `--agent codex` | **The default is a single per-repo setting** — no global default, no shipped @@ -197,17 +199,26 @@ in a repo with no default yet. Everything is registry-driven — no ranking, no call; the default is a simple, explicit choice (the hook where future task-aware routing can plug in). -**Non-Claude workers degrade honestly.** codex/grok have no work-system skills, -so a launched worker gets a bootstrap prompt (read `TASK.md`, commit, open a PR) -instead of `/continue`. Everything git/PR-derived (`/status`, `/list`, the +**Non-Claude workers degrade honestly.** codex/grok/kimi have no work-system +skills, so a launched worker gets a bootstrap prompt (read `TASK.md`, commit, open +a PR) instead of `/continue`. Everything git/PR-derived (`/status`, `/list`, the `[ws]` statusline, `/close`'s tab teardown) works for any worker; only claude-session concepts differ. `/continue`'s reopen **always sends `claude -c`** — the work-system doesn't persist which worker a task used (per-task agent memory is a later idea), so it can't dispatch per CLI. That resumes a claude worker; for -a codex/grok task it's a *new* Claude session, so you resume the real worker -yourself in the tab (`codex resume --last` / `grok -c`) — `/continue` surfaces -this caveat inline. Since both CLIs read `AGENTS.md`, dropping a short `AGENTS.md` -note into the worktree is an optional way to give them standing task guidance. +a codex/grok/kimi task it's a *new* Claude session, so you resume the real worker +yourself in the tab (`codex resume --last` / `grok -c` / `kimi -c`) — `/continue` +surfaces this caveat inline. Since codex and grok read `AGENTS.md`, dropping a +short `AGENTS.md` note into the worktree is an optional way to give them standing +task guidance. + +**kimi launches in two phases.** It has no positional launch prompt, and its +one-shot `-p` flag can't be combined with the autonomous `--auto`/`-y` modes — so +the worker is `sh -c 'kimi -m "$1" -p "$2"; exec kimi -c --auto' …`: the `-p` seed +works the task through once (it runs tools unattended), then `exec` hands over to +the interactive autonomous session, which inherits the seed's full history. So +unlike the other workers, a kimi tab has already made progress by the time you +switch to it. ## herdr integration @@ -225,7 +236,7 @@ Inside herdr, `/kickoff` doesn't just create the worktree and print manual instructions — it opens a new herdr **tab** in the *same* workspace, with the worktree as its cwd, and starts the task there for you. `/adopt` does exactly the same once it has created the worktree from an existing branch — same helper, same -tab, same worker selection (`--opus`/`--sol`/`--grok`/`--pick`, or the repo default); +tab, same worker selection (`--opus`/`--sol`/`--grok`/`--kimi`/`--pick`, or the repo default); its tab label comes from the *resolved* task name, so it's sensible even when `/adopt` keeps the original branch name rather than renaming it to `task/`: @@ -238,8 +249,10 @@ keeps the original branch name rather than renaming it to `task/`: - The chosen worker is launched directly as argv (`herdr agent start … -- `), so the real CLI process is what herdr's agent-state detection sees. A claude worker gets `claude --model -n "