Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>/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-<name>/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/<name>/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-<name>/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/<project>/<session>/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.
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>/SKILL.md`; the MCP registration is read
from `~/.kimi-code/mcp.json`. Its transcripts (`~/.kimi/sessions/<project>/
<session>/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/<name>/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.
Expand Down
28 changes: 26 additions & 2 deletions src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub enum Harness {
OpenClaude,
Codex,
Opencode,
Kimi,
Antigravity,
Goose,
CopilotCli,
Expand Down Expand Up @@ -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"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid installing new-root skills for legacy-only Kimi

When a user has only the explicitly supported pre-migration ~/.kimi root, this probe marks Kimi present, but install always writes to ~/.kimi-code/skills; write_managed creates that parent and reports success even though the detected legacy installation does not use the new configuration root. Restrict command installation to a ~/.kimi-code hit, or select the command surface based on which root was detected.

AGENTS.md reference: AGENTS.md:L273-L273

Useful? React with 👍 / 👎.

sessions_dir: Some(".kimi/sessions"),
transcript: TranscriptSupport::NotImplemented {
detail: "kimi writes sessions/<project>/<session>/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",
Expand Down Expand Up @@ -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/<name>/SKILL.md`.
command_surface: CommandSurface::Skill {
dir: ".qwen/skills",
},
Comment on lines +300 to 302

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the canonical harness documentation

Making Qwen a writable skill surface and adding Kimi leaves doc/engram.texi's harness table stale: it omits Kimi, still marks Qwen as having no command surface, and still describes three unsupported harnesses out of eight. The root agent context also still says there are seven harnesses and only three writable surfaces, so users and agents consulting the documented capability matrix receive the opposite of the new behavior; update these support tables and counts with the registry change.

AGENTS.md reference: AGENTS.md:L33-L33

Useful? React with 👍 / 👎.

mcp_config: Some(McpConfigSource::Json(".qwen/settings.json")),
hooks_config: None,
Expand Down
9 changes: 4 additions & 5 deletions src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
35 changes: 34 additions & 1 deletion tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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/<name>/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}");
}
}
Loading