Skip to content
98 changes: 48 additions & 50 deletions internal/agents/kimi/adapter.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Package kimi provides Kimi Code CLI agent integration.
//
// Integration Note:
// This adapter natively relies on Astral's `uv` package manager
// (`uv tool install kimi-cli`) to securely download and run Kimi CLI,
// avoiding upstream's pipe-to-shell bootstrap scripts.
// This adapter targets the modern Kimi Code CLI (v0.18.0+, TypeScript/npm-based).
// Legacy Python/uv-based kimi-cli is no longer supported; users should migrate
// via `kimi migrate` before running gentle-ai install.
package kimi

import (
Expand Down Expand Up @@ -95,7 +95,8 @@ func (a *Adapter) findKimi() (string, error) {
if runtime.GOOS == "windows" {
fallbacks = append(fallbacks,
filepath.Join(home, "AppData", "Local", "Microsoft", "WinGet", "Links", "kimi.exe"),
filepath.Join(home, "AppData", "Roaming", "uv", "bin", "kimi.exe"),
filepath.Join(home, "AppData", "Roaming", "npm", "kimi.cmd"),
filepath.Join(home, "AppData", "Roaming", "npm", "kimi.exe"),
)
}

Expand Down Expand Up @@ -125,34 +126,28 @@ func (a *Adapter) InstallCommand(profile system.PlatformProfile) ([][]string, er
// --- Config paths ---

func (a *Adapter) GlobalConfigDir(homeDir string) string {
return filepath.Join(homeDir, ".kimi")
return filepath.Join(homeDir, ".kimi-code")
}

func (a *Adapter) SystemPromptDir(homeDir string) string {
return filepath.Join(homeDir, ".kimi")
return filepath.Join(homeDir, ".kimi-code")
}

func (a *Adapter) SystemPromptFile(homeDir string) string {
return filepath.Join(homeDir, ".kimi", "KIMI.md")
return filepath.Join(homeDir, ".kimi-code", "AGENTS.md")
}

// SkillsDir returns the shared skills directory path.
// SkillsDir returns the Kimi-native skills directory path.
//
// Kimi Code CLI supports native Agent Skills. It recognizes both:
// - native brand-specific skills: ~/.kimi/skills
// - generic shared skills: ~/.config/agents/skills and ~/.agents/skills
//
// We intentionally use ~/.config/agents/skills here as a cross-agent shared
// convention. Kimi will discover this directory natively as part of its
// generic skills group (the docs mark this path as "recommended").
//
// See: https://moonshotai.github.io/kimi-cli/en/customization/skills.html
// Kimi Code CLI v0.18.0+ discovers skills in ~/.kimi-code/skills natively.
// We also register the generic ~/.config/agents/skills path in the skill
// registry so cross-agent skills are shared.
func (a *Adapter) SkillsDir(homeDir string) string {
return filepath.Join(homeDir, ".config", "agents", "skills")
return filepath.Join(homeDir, ".kimi-code", "skills")
}

func (a *Adapter) SettingsPath(homeDir string) string {
return filepath.Join(homeDir, ".kimi", "config.toml")
return filepath.Join(homeDir, ".kimi-code", "config.toml")
}

func (a *Adapter) CommandsDir(string) string {
Expand All @@ -162,7 +157,7 @@ func (a *Adapter) CommandsDir(string) string {
// --- Config strategies ---

func (a *Adapter) SystemPromptStrategy() model.SystemPromptStrategy {
return model.StrategyJinjaModules
return model.StrategyFileReplace
}

func (a *Adapter) MCPStrategy() model.MCPStrategy {
Expand All @@ -172,7 +167,7 @@ func (a *Adapter) MCPStrategy() model.MCPStrategy {
// --- MCP ---

func (a *Adapter) MCPConfigPath(homeDir string, _ string) string {
return filepath.Join(homeDir, ".kimi", "mcp.json")
return filepath.Join(homeDir, ".kimi-code", "mcp.json")
}

// --- Optional capabilities ---
Expand Down Expand Up @@ -203,31 +198,39 @@ func (a *Adapter) SupportsMCP() bool {

// --- Sub-agent support (optional interface) ---
//
// Kimi uses YAML-based agent specs with separate .md system prompts.
// The SDD component copies all files from the embedded agents directory.
// Kimi Code CLI has built-in subagents (coder, explore, plan) that are
// native to the tool. Gentle AI SDD agents are injected via the AGENTS.md
// system prompt and skill registry, not via separate YAML specs.

func (a *Adapter) SupportsSubAgents() bool {
return true
}

func (a *Adapter) SubAgentsDir(homeDir string) string {
return filepath.Join(homeDir, ".kimi", "agents")
return filepath.Join(homeDir, ".kimi-code", "agents")
}

func (a *Adapter) EmbeddedSubAgentsDir() string {
return "kimi/agents"
}

func (a *Adapter) PostInstallMessage(homeDir string) string {
gentlemanYaml := filepath.Join(homeDir, ".kimi", "agents", "gentleman.yaml")
skillsRoot := filepath.Join(homeDir, ".config", "agents", "skills")
agentsPath := filepath.Join(homeDir, ".kimi-code", "AGENTS.md")
skillsRoot := filepath.Join(homeDir, ".kimi-code", "skills")
mcpPath := filepath.Join(homeDir, ".kimi-code", "mcp.json")

return fmt.Sprintf(`Kimi Code CLI configured!

return fmt.Sprintf(`Kimi Code configured!
System prompt:
"%s"

Usage:
kimi --agent-file "%s"
Skills root:
"%s"

MCP config:
"%s"

Native SDD entrypoints:
Native SDD entrypoints (via skills):
/skill:sdd-init
/skill:sdd-explore
/skill:sdd-propose
Expand All @@ -237,13 +240,9 @@ Native SDD entrypoints:
/skill:sdd-apply
/skill:sdd-verify
/skill:sdd-archive
/skill:sdd-onboard

Skills root:
"%s"`, gentlemanYaml, skillsRoot)
/skill:sdd-onboard`, agentsPath, skillsRoot, mcpPath)
}


// --- Helpers ---

func defaultStat(path string) statResult {
Expand All @@ -261,7 +260,7 @@ func defaultPathExists(path string) bool {

// ConfigPath returns the configuration directory path.
func ConfigPath(homeDir string) string {
return filepath.Join(homeDir, ".kimi")
return filepath.Join(homeDir, ".kimi-code")
}

func binaryName() string {
Expand All @@ -271,27 +270,28 @@ func binaryName() string {
return "kimi"
}

// BootstrapTemplate ensures the base KIMI.md template exists in the agent's config directory.
// It is used by the installation pipeline to guarantee that modular components
// (SDD, Engram) can be included even if the Persona component is not installed.
// BootstrapTemplate ensures the base AGENTS.md template exists in the agent's
// config directory. It is used by the installation pipeline to guarantee that
// modular components (SDD, Engram) can be included even if the Persona
// component is not installed.
func (a *Adapter) BootstrapTemplate(homeDir string) error {
kimiDir := a.GlobalConfigDir(homeDir)
if err := os.MkdirAll(kimiDir, 0o755); err != nil {
return fmt.Errorf("create kimi config dir: %w", err)
}

skeletonPath := a.SystemPromptFile(homeDir)

// We always write the skeleton to ensure any missing includes are restored.
// Since KIMI.md is the 'router' for modular Jinja components, it should
// remain managed by the framework.
content := assets.MustRead("kimi/KIMI.md")
if _, err := filemerge.WriteFileAtomic(skeletonPath, []byte(content), 0o644); err != nil {
return fmt.Errorf("write KIMI.md skeleton: %w", err)

// We write the skeleton only if AGENTS.md does not already exist so we
// do not clobber user-authored instructions on a re-install.
if _, err := os.Stat(skeletonPath); os.IsNotExist(err) {
content := assets.MustRead("kimi/AGENTS.md")
if _, err := filemerge.WriteFileAtomic(skeletonPath, []byte(content), 0o644); err != nil {
return fmt.Errorf("write AGENTS.md skeleton: %w", err)
}
}
Comment on lines +285 to 292

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate non-NotExist stat failures in bootstrap.

At Line 287, non-ENOENT os.Stat errors are ignored, so permission/I/O failures can silently skip AGENTS.md creation and continue as “success.”

Suggested fix
-    if _, err := os.Stat(skeletonPath); os.IsNotExist(err) {
-        content := assets.MustRead("kimi/AGENTS.md")
-        if _, err := filemerge.WriteFileAtomic(skeletonPath, []byte(content), 0o644); err != nil {
-            return fmt.Errorf("write AGENTS.md skeleton: %w", err)
-        }
-    }
+    if _, err := os.Stat(skeletonPath); err != nil {
+        if !os.IsNotExist(err) {
+            return fmt.Errorf("stat AGENTS.md skeleton path: %w", err)
+        }
+        content := assets.MustRead("kimi/AGENTS.md")
+        if _, err := filemerge.WriteFileAtomic(skeletonPath, []byte(content), 0o644); err != nil {
+            return fmt.Errorf("write AGENTS.md skeleton: %w", err)
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// We write the skeleton only if AGENTS.md does not already exist so we
// do not clobber user-authored instructions on a re-install.
if _, err := os.Stat(skeletonPath); os.IsNotExist(err) {
content := assets.MustRead("kimi/AGENTS.md")
if _, err := filemerge.WriteFileAtomic(skeletonPath, []byte(content), 0o644); err != nil {
return fmt.Errorf("write AGENTS.md skeleton: %w", err)
}
}
// We write the skeleton only if AGENTS.md does not already exist so we
// do not clobber user-authored instructions on a re-install.
if _, err := os.Stat(skeletonPath); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("stat AGENTS.md skeleton path: %w", err)
}
content := assets.MustRead("kimi/AGENTS.md")
if _, err := filemerge.WriteFileAtomic(skeletonPath, []byte(content), 0o644); err != nil {
return fmt.Errorf("write AGENTS.md skeleton: %w", err)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agents/kimi/adapter.go` around lines 285 - 292, The os.Stat call
checking for skeletonPath only handles the os.IsNotExist case and silently
ignores other error types like permission or I/O failures. Modify the error
handling logic to first check if err is not nil, then verify if it is
specifically os.IsNotExist before proceeding with the file creation. If the
error is any other type (permission denied, I/O error, etc.), return it as an
error instead of silently continuing execution.


// Kimi considers config.toml a required file. We create an empty one if
// it's missing to satisfy verification during a minimalist install.
// Kimi Code CLI expects config.toml to exist.
configPath := a.SettingsPath(homeDir)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if _, err := filemerge.WriteFileAtomic(configPath, []byte("# Kimi Code Config\n"), 0o644); err != nil {
Expand All @@ -301,5 +301,3 @@ func (a *Adapter) BootstrapTemplate(homeDir string) error {

return nil
}


1 change: 1 addition & 0 deletions internal/assets/kimi/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- Gentle AI managed agent instructions for Kimi Code CLI. -->
101 changes: 101 additions & 0 deletions internal/assets/kimi/engram-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
## Engram Persistent Memory — Protocol

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve markdown heading-structure lint warnings.

Line 1 starts with a level-2 heading, and Lines 74/77/80/83/86/89 are missing the required surrounding blank lines per markdownlint hints.

Also applies to: 74-90

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/assets/kimi/engram-protocol.md` at line 1, The markdown file has two
structural issues to fix: First, change the level-2 heading at the start of the
document (## Engram Persistent Memory — Protocol) to a level-1 heading using a
single hash mark (#) instead of double hash marks. Second, ensure all section
headings at lines 74, 77, 80, 83, 86, and 89 have blank lines before and after
them to comply with markdownlint requirements. This means adding an empty line
above and below each of these headings to properly separate them from
surrounding content.

Source: Linters/SAST tools


You have access to Engram, a persistent memory system that survives across sessions and compactions.
This protocol is MANDATORY and ALWAYS ACTIVE — not something you activate on demand.

### PROACTIVE SAVE TRIGGERS (mandatory — do NOT wait for user to ask)

Call `mem_save` IMMEDIATELY and WITHOUT BEING ASKED after any of these:
- Architecture or design decision made
- Team convention documented or established
- Workflow change agreed upon
- Tool or library choice made with tradeoffs
- Bug fix completed (include root cause)
- Feature implemented with non-obvious approach
- Notion/Jira/GitHub artifact created or updated with significant content
- Configuration change or environment setup done
- Non-obvious discovery about the codebase
- Gotcha, edge case, or unexpected behavior found
- Pattern established (naming, structure, convention)
- User preference or constraint learned

Self-check after EVERY task: "Did I make a decision, fix a bug, learn something non-obvious, or establish a convention? If yes, call mem_save NOW."

Format for `mem_save`:
- **title**: Verb + what — short, searchable (e.g. "Fixed N+1 query in UserList")
- **type**: bugfix | decision | architecture | discovery | pattern | config | preference
- **scope**: `project` (default) | `personal`
- **topic_key** (recommended for evolving topics): stable key like `architecture/auth-model`
- **capture_prompt**: optional; default `true`. Do not set this for normal human/proactive saves. Set `false` only for automated artifacts such as SDD proposal/spec/design/tasks/apply/verify/archive/init reports, testing-capabilities caches, onboarding/state artifacts, or skill-registry output.
- **content**:
- **What**: One sentence — what was done
- **Why**: What motivated it (user request, bug, performance, etc.)
- **Where**: Files or paths affected
- **Learned**: Gotchas, edge cases, things that surprised you (omit if none)

Prompt capture behavior (Engram v1.15.3+):
- `mem_save` captures the user prompt best-effort when the MCP process already has prompt context for the same `project + session_id`.
- `mem_save` never invents prompt text. If no prompt context exists, the save still succeeds without prompt capture.
- `mem_save_prompt` records the prompt and feeds SessionActivity so later `mem_save` calls can capture and dedupe it.
- If an agent/plugin hook can observe the user's prompt before derived memory saves happen, it should call `mem_save_prompt` first.
- Do not decide prompt capture by `type`; SDD artifacts also use `architecture`, and human decisions can too. Use explicit `capture_prompt: false` for automated artifacts.
- If an older Engram tool schema does not expose `capture_prompt`, omit the field rather than failing.

Topic update rules:
- Different topics MUST NOT overwrite each other
- Same topic evolving → use same `topic_key` (upsert)
- Unsure about key → call `mem_suggest_topic_key` first
- Know exact ID to fix → use `mem_update`

Memory lifecycle rule (when Engram exposes lifecycle metadata/tooling):
- At session start or before architecture-sensitive work, call `mem_review` with action `list` for the current project when the tool is available.
- If `mem_review` is unavailable, do not fail the task. Continue with normal `mem_context`/`mem_search`, and still apply lifecycle metadata from any returned observations when present.
- `active` memories may be used normally.
- `needs_review` memories are stale context, not trusted facts.
- When a retrieved memory is marked `needs_review`, surface that stale context to the user and verify it against current evidence before relying on it.
- Do NOT call `mem_review` with action `mark_reviewed` automatically. Only call `mark_reviewed` after explicit user confirmation or through a dedicated memory maintenance command.

### WHEN TO SEARCH MEMORY

On any variation of "remember", "recall", "what did we do", "how did we solve", or references to past work (in any language the user writes in):
1. Call `mem_context` — checks recent session history (fast, cheap)
2. If not found, call `mem_search` with relevant keywords
3. If found, use `mem_get_observation` for full untruncated content

Also search PROACTIVELY when:
- Starting work on something that might have been done before
- User mentions a topic you have no context on
- User's FIRST message references the project, a feature, or a problem — call `mem_search` with keywords from their message to check for prior work before responding

### SESSION CLOSE PROTOCOL (mandatory)

Before ending a session or saying "done" / "that's it" (or the equivalent in the user's language), call `mem_session_summary`:

## Goal
[What we were working on this session]

## Instructions
[User preferences or constraints discovered — skip if none]

## Discoveries
- [Technical findings, gotchas, non-obvious learnings]

## Accomplished
- [Completed items with key details]

## Next Steps
- [What remains to be done — for the next session]

## Relevant Files
- path/to/file — [what it does or what changed]

This is NOT optional. If you skip this, the next session starts blind.

### AFTER COMPACTION

If you see a compaction message or "FIRST ACTION REQUIRED":
1. IMMEDIATELY call `mem_session_summary` with the compacted summary content — this persists what was done before compaction
2. Call `mem_context` to recover additional context from previous sessions
3. Only THEN continue working

Do not skip step 1. Without it, everything done before compaction is lost from memory.
Loading