From 9b908ccff3f480c783832469c76e1d576a738cfb Mon Sep 17 00:00:00 2001 From: UnbreakableMJ Date: Mon, 24 Aug 2026 12:33:28 +0300 Subject: [PATCH] Add Kimi, and stop dismissing Qwen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kimi is the ninth harness. Its configuration and its sessions live in different roots — `~/.kimi-code` holds config and the MCP registration, `~/.kimi` still holds sessions and carries a `.migrated-to-kimi-code` marker — so both are probed and a user on either side of that migration is detected. Skills go to `~/.kimi-code/skills/engram-/SKILL.md`. Its transcripts are `~/.kimi/sessions///context.jsonl`: line-oriented, one record per message, entirely readable. The reader is still `NotImplemented`, but now for a stated reason rather than an unexamined one — the project directory is a hash with no published mapping back to a working directory, and engram's scope resolution is by cwd. Qwen's classification was simply stale. It claimed "command format is unverified", which was true when written and had never been rechecked; Qwen Code ships `docs/features/skills.md`, which documents `~/.qwen/skills//SKILL.md`, and that directory already exists and is writable. Verified against the vendor's own documentation, not inferred. Seven of nine harnesses can now host something, up from five. Goose and Copilot CLI remain genuinely unsurveyed and say so in their own words. Gates: fmt, clippy -D warnings, 257 tests, REUSE 3.3, makeinfo clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016i16R4GhdSffsboRYq97Fs --- AGENTS.md | 3 ++- CHANGELOG.md | 11 +++++++++++ src/harness.rs | 28 ++++++++++++++++++++++++++-- src/install.rs | 9 ++++----- tests/cli.rs | 35 ++++++++++++++++++++++++++++++++++- 5 files changed, 77 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 41dfac9..f3356ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -269,7 +269,8 @@ Two readers exist: `claude_code` and `codex`. Adding a third means adding a `Rea - **`plugins/engram/` is the single source of truth.** `install.rs` embeds the command bodies with `include_str!`, so the plugin directory and the installed files cannot drift and the compiler enforces the files exist. Exactly two substitutions, via `str::replace`, no template engine: `{{DB}}` and `{{HARNESS}}`. - **`{{DB}}` is load-bearing.** The path is discovered from the harness's *own* MCP registration (`harness::registered_db`) — on a typical host all writable harnesses point at one shared store (here `~/.local/share/engram/engram.db`) — but see the drift note below: what they registered *yesterday* is not necessarily what a previously-generated command still pins. A generated command that omitted `--db` would fall back to clap's relative `engram.db` default and quietly write to a different store than the agents read. Config formats are scanned narrowly rather than deserialized: JSON (`mcpServers`, or Opencode's `mcp`), **JSONC** (comment-stripped by a string-aware pass — a `//` inside `"https://…"` must survive), and TOML (line-scanned, so engram needs no TOML dependency). Engram **reads** JSONC and never rewrites it; a serde round-trip would delete the user's comments. -- **5 of 8 harnesses can host something; only 3 host a *command*.** Claude Code, **OpenClaude**, and Opencode have writable command dirs. **Codex 0.149 removed `~/.codex/prompts/`** — the binary contains no such string — and moved to skills at `~/.codex/skills//SKILL.md`, discovered automatically with nothing to register; engram writes there now. It wrote prompt files nobody read for a release, which is exactly what a harness table drifting from reality looks like. **Antigravity has no slash-command directory at all** — its extension surface is skills, packaged in plugins, and `agy plugin validate` reports a plugin's `commands/` as "2 processed (converted to skills)", so a command there is a skill either way. Engram writes it a plugin (`~/.gemini/config/plugins/engram/`: `plugin.json` + one `skills/engram-/SKILL.md` per command). Goose, Copilot CLI, and Qwen have nothing engram can write and each says so **in its own words** — one shared sentence described none of them accurately. +- **7 of 9 harnesses can host something; only 3 host a *command*.** Claude Code, **OpenClaude**, and Opencode have writable command dirs. **Codex 0.149 removed `~/.codex/prompts/`** — the binary contains no such string — and moved to skills at `~/.codex/skills//SKILL.md`, discovered automatically with nothing to register; engram writes there now. It wrote prompt files nobody read for a release, which is exactly what a harness table drifting from reality looks like. **Antigravity has no slash-command directory at all** — its extension surface is skills, packaged in plugins, and `agy plugin validate` reports a plugin's `commands/` as "2 processed (converted to skills)", so a command there is a skill either way. Engram writes it a plugin (`~/.gemini/config/plugins/engram/`: `plugin.json` + one `skills/engram-/SKILL.md` per command). **Kimi** and **Qwen** take skills in their own config roots (`~/.kimi-code/skills`, `~/.qwen/skills`) — Qwen's was verified against its own bundled `docs/features/skills.md`, having previously been dismissed as "format unverified". Goose and Copilot CLI have nothing engram can write and each says so **in its own words** — one shared sentence described none of them accurately. +- **Kimi's configuration and its sessions live in different roots.** `~/.kimi-code` holds config and MCP registration; `~/.kimi` still holds `sessions///context.jsonl` and carries a `.migrated-to-kimi-code` marker. Both are probed, so a user on either side of that migration is detected. The transcript is line-oriented and readable, but the project directory is a hash with no published mapping back to a working directory — which is why the reader is `NotImplemented` with that stated, rather than guessed at. - **OpenClaude is a Claude Code fork** (`@gitlawb/openclaude`) with its own config root. Its MCP registration lives in `~/.openclaude.json` — the `~/.claude.json` analogue — **not** `~/.openclaude/settings.json`, which holds env/model/hooks and no servers block. Its transcripts are Claude Code's format down to the record keys, so `ReaderKind::ClaudeCode` serves both; the fork-only record types (`mode`, `file-history-snapshot`, `last-prompt`) are already in the non-message allowlist and must stay there, since a fork tripping `unknown_record` every run would train the reader to ignore its own drift alarm. - **`CommandSurface` is an enum, not a bool.** `Markdown { dir, file, frontmatter }`, `Skill { dir }` (a bare skills root, scanned directly — Codex), `Plugin { dir }` (a plugin wrapping skills — Antigravity), `None { detail }`. Antigravity broke the old `command_frontmatter: bool` because the *shape* of the artifact differs, not just its header — and `None` carries a per-harness reason. - **Frontmatter is per-surface.** `Markdown { frontmatter: false }` exists for a harness whose command files are plain markdown and would otherwise render the YAML block as literal text; no shipped harness uses it since Codex moved to skills, and it stays because the next one may. A `Skill` or `Plugin` skill has a *different* contract again — `name` + `description`, no `argument-hint`, no `allowed-tools` — and lifts its description from the shared template so the two surfaces cannot describe the same command differently. diff --git a/CHANGELOG.md b/CHANGELOG.md index 979a34d..2292ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,17 @@ follows [Keep a Changelog](https://keepachangelog.com/); versions follow ### Added +- **Kimi** is a supported harness (the ninth). Skills go to + `~/.kimi-code/skills/engram-/SKILL.md`; the MCP registration is read + from `~/.kimi-code/mcp.json`. Its transcripts (`~/.kimi/sessions// + /context.jsonl`) are line-oriented and readable, but the project + directory is a hash with no published mapping back to a working directory, so + the reader stays `NotImplemented` with that reason recorded. +- **Qwen now gets skills.** It was reported as having no writable surface on the + grounds that its "command format is unverified"; Qwen Code's own bundled + `docs/features/skills.md` documents `~/.qwen/skills//SKILL.md`, so the + claim was stale rather than true. + - `CommandSurface::Skill { dir }` — a bare skills root scanned directly by the harness, distinct from `Plugin { dir }`, which wraps skills in a plugin directory with a manifest. diff --git a/src/harness.rs b/src/harness.rs index 812d59e..336a18b 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -37,6 +37,7 @@ pub enum Harness { OpenClaude, Codex, Opencode, + Kimi, Antigravity, Goose, CopilotCli, @@ -223,6 +224,26 @@ pub const ALL: &[HarnessSpec] = &[ mcp_config: Some(McpConfigSource::Jsonc(".config/opencode/opencode.jsonc")), hooks_config: None, }, + HarnessSpec { + id: Harness::Kimi, + name: "kimi", + // `~/.kimi` is the pre-migration root and still holds sessions; the + // `.migrated-to-kimi-code` marker inside it points at `~/.kimi-code`, + // which is where configuration lives now. Probing both means a user on + // either side of that migration is detected. + probe: &[".kimi-code", ".kimi"], + sessions_dir: Some(".kimi/sessions"), + transcript: TranscriptSupport::NotImplemented { + detail: "kimi writes sessions///context.jsonl, a line-oriented \ + conversation engram could read; the project directory is a hash with no \ + published mapping back to a working directory", + }, + command_surface: CommandSurface::Skill { + dir: ".kimi-code/skills", + }, + mcp_config: Some(McpConfigSource::Json(".kimi-code/mcp.json")), + hooks_config: None, + }, HarnessSpec { id: Harness::Antigravity, name: "antigravity", @@ -273,8 +294,11 @@ pub const ALL: &[HarnessSpec] = &[ transcript: TranscriptSupport::NotImplemented { detail: "qwen's session storage has not been surveyed", }, - command_surface: CommandSurface::None { - detail: "qwen's command format is unverified; engram will not guess at it", + // Verified against Qwen Code's own bundled documentation + // (`docs/features/skills.md`): personal skills live in + // `~/.qwen/skills//SKILL.md`. + command_surface: CommandSurface::Skill { + dir: ".qwen/skills", }, mcp_config: Some(McpConfigSource::Json(".qwen/settings.json")), hooks_config: None, diff --git a/src/install.rs b/src/install.rs index 8156ab0..7710432 100644 --- a/src/install.rs +++ b/src/install.rs @@ -852,11 +852,10 @@ mod tests { // work. Antigravity used to be one of these; it now has a plugin // surface, so the exclusion moved to the harnesses that really have // none. - for id in [ - harness::Harness::Goose, - harness::Harness::CopilotCli, - harness::Harness::Qwen, - ] { + // Qwen used to be here; its bundled docs confirmed a real skills + // directory, so it moved to a surface. Goose and Copilot CLI remain + // genuinely unsurveyed. + for id in [harness::Harness::Goose, harness::Harness::CopilotCli] { assert!( !targets.iter().any(|s| s.id == id), "{id:?} has no command surface and must not be a default target" diff --git a/tests/cli.rs b/tests/cli.rs index fdb4e51..818fdc5 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2095,7 +2095,7 @@ fn install_list_reports_every_harness_and_writes_nothing() { let data = parse_single_line_json(&assert.get_output().stdout)["data"].clone(); let harnesses = data["harnesses"].as_array().expect("harnesses"); - assert_eq!(harnesses.len(), 8, "every known harness must be reported"); + assert_eq!(harnesses.len(), 9, "every known harness must be reported"); let claude = harnesses .iter() @@ -3347,3 +3347,36 @@ fn install_skips_an_unwritable_target_and_keeps_going() { std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o755); std::fs::set_permissions(&skills, perms).ok(); } + +/// Kimi and Qwen both take skills, in their own config roots. +/// +/// Qwen was previously reported as having no surface at all ("command format +/// unverified"); its own bundled `docs/features/skills.md` documents +/// `~/.qwen/skills//SKILL.md`, so the claim was simply stale. Kimi's +/// configuration moved to `~/.kimi-code` while its sessions stayed in +/// `~/.kimi`, which is why both are probed. +#[test] +fn install_writes_kimi_and_qwen_skills() { + let tmp = TempDir::new().expect("tempdir"); + let db = tmp.path().join("test.db"); + let home = tmp.path().join("home"); + std::fs::create_dir_all(&home).expect("create fake home"); + pretend_installed(&home, ".kimi-code"); + pretend_installed(&home, ".qwen"); + + install(&db, &home, &["--db-path", "/shared/engram.db"]) + .assert() + .success(); + + for (harness, path) in [ + ("kimi", ".kimi-code/skills/engram-save-chat/SKILL.md"), + ("qwen", ".qwen/skills/engram-save-chat/SKILL.md"), + ] { + let text = std::fs::read_to_string(home.join(path)) + .unwrap_or_else(|e| panic!("{harness}: {path}: {e}")); + assert!(text.starts_with("---\n"), "{harness}: {text}"); + assert!(text.contains("name: engram-save-chat"), "{harness}"); + assert!(text.contains("--db /shared/engram.db"), "{harness}"); + assert!(text.contains(&format!("--harness {harness}")), "{harness}"); + } +}