Repository navigation
feat(core): discover kilo.json and .kilo config directories - #15018
Conversation
Read kilo.json and kilo.jsonc next to opencode.json and opencode.jsonc in the Kilo v1 orders. Discover .kilocode and .kilo project directories instead of .opencode, with .kilo winning. Stop the upward walk at the worktree root. Write new global config to kilo.jsonc and let kilo mcp add use the Kilo v1 candidates.
Kilo does not read .opencode project directories, so the compiled service smoke must create its plugin in .kilo/plugins.
# Conflicts: # packages/core/src/config.ts
|
➖ v2 migration progress: 0.9% → 0.9% (0 done, 0 total, 0 dropped; 86/9829 cells) Placement: 125 new marked lines in shared files (125 seam), 0 new Ratchets: 9 core-internal imports (base 8); shared patches by rung 29 seam, 1 patch (base 18 seam, 1 patch). No change to the progress map. |
| // Kilo v1 reads config files in three different orders. For reads, a later file in each list wins. | ||
|
|
||
| /** The global config directory and the project root files of one directory. `.jsonc` wins over `.json`. */ | ||
| export const names = ["kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc"] |
There was a problem hiding this comment.
SUGGESTION: Legacy global config.json is no longer read or written
Kilo v1 also reads config.json in the global config directory (lowest priority) and its globalConfigFile includes config.json as a write candidate; the v1 TOML migration also writes that file. Here names is shared by the global directory and project-root discovery, so it cannot simply be appended (every project root would then read it). If dropping the legacy file is intentional, noting it in the body would help; otherwise it needs a global-only entry in the read chain and in Config.update's candidate list.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| project: yield* Effect.forEach( | ||
| visible.filter((item) => path.basename(item) === ".opencode").toReversed(), | ||
| // kilocode_change - Kilo project directories replace .opencode | ||
| visible.filter((item) => directoryNames.includes(path.basename(item))).toReversed(), |
There was a problem hiding this comment.
SUGGESTION: Confirm the cross-level precedence for .kilo directories
Kilo v1 merges config directories nearest-first (ConfigPaths.directories does not reverse the fs.up result), so an ancestor .kilo overrides a nested one. The pre-existing .toReversed() here makes the nearest .kilo win, which matches the root-file behavior and v2 conventions but differs from v1 for nested projects. If that is intentional, a cross-level test and a line in the PR body would pin it down.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| path.join(directory, ".opencode", "opencode.jsonc"), | ||
| ] | ||
| // kilocode_change start - Kilo config names, with .kilo and .kilocode before root files, like Kilo v1 | ||
| const candidates = [".kilo", ".kilocode", "."].flatMap((parent) => |
There was a problem hiding this comment.
SUGGESTION: resolveConfigPath now probes .kilo/.kilocode inside the global config directory
v1's resolveConfigPath(baseDir, global) only checks the root files when global is true. Both mcp add --global and plugin add/plugin remove call this with global.config, so a nested .kilo/kilo.jsonc there would now be selected and written even though the global loader never reads it. Keeping a global flag like v1 would preserve the distinction. The function is exported and has no unit test; a small fs-fixture test would cover both modes.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: 1 Issue Found | Recommendation: Merge after optional test addition Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (2 incremental files)
Incremental verification
Fix these issues in Kilo Cloud Previous Review Summaries (3 snapshots, latest commit 0ac8fe3)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 0ac8fe3)Status: 1 Issue Found | Recommendation: Merge after optional test addition Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (7 files)
Incremental verification
Fix these issues in Kilo Cloud Previous review (commit 57e709e)Status: 1 Issue Found | Recommendation: Merge after optional test addition Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (13 files)
Incremental verification
No security or memory-leak concerns in the incremental changes: the new branch allocates only path strings and the discovery walk holds no long-lived state. Automated lint/typecheck/tests were not run in this read-only review. Fix these issues in Kilo Cloud Previous review (commit b90dbab)Status: 3 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (15 files)
Incremental changes since Reviewed by deepseek-v4.1-flash-1017 · Input: 38.2K · Output: 3.7K · Cached: 226.4K Review guidance: REVIEW.md from base branch |
| ] | ||
| export async function resolveConfigPath(directory: string, global = false) { | ||
| // kilocode_change start - Kilo config names, with .kilo and .kilocode before root files, like Kilo v1; the global directory only reads root files, like Kilo v1 | ||
| const candidates = global |
There was a problem hiding this comment.
SUGGESTION: The new global branch has no automated test
The global mode was added to fix the write-path bug, and it now has two distinct candidate lists plus a fallback that differs by mode (kilo.jsonc/root files for global, kilo.json as the fallback). The only coverage is the manual kilo mcp add --global self-test in the PR body; plugin add/plugin remove, which now pass true, are not exercised. resolveConfigPath is exported, pure filesystem I/O, and easy to cover with a temp directory: assert global mode ignores .kilo/ and .kilocode/, non-global prefers them, and the fallback path differs per mode.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
docs(migration): record U-E0-02 to U-E0-04 status and the #15018 follow-up
Plan unit U-E1-07 (Kilo config layer), part 1: config file names and
.kilodirectory discovery.What Problem This Solves
The v2 base only knows OpenCode conventions. It reads
opencode.jsonandopencode.jsonc, discovers.opencode/project directories, and walks above the repository root. Kilo users keep their config inkilo.json,kilo.jsonc,.kilo/and the legacy.kilocode/. On v2 none of that loads, so project config, commands, agents, modes, skills and plugins from Kilo directories are lost.Why This Change Was Made
The behavior follows v1 Kilo on
main(767b958). v1 reads files in three different orders, and this PR keeps all three. In each row a later file wins.kilo.json,kilo.jsonc,opencode.json,opencode.jsoncconfig.ts:460-481kilo.*(root to nearest,.jsonthen.jsonc), then allopencode.*the same way, so anyopencode.*wins over anykilo.*at any depthconfig.ts:786,paths.ts:10-21.kilocodeand.kilokilo.jsonc,kilo.json,opencode.jsonc,opencode.json(ALL_CONFIG_FILES)kilocode/config/config.ts:41,config.ts:837Other v1 rules this PR applies:
.kilocodeand.kiloare both read at each level, and.kilowins..opencodeis not read, as in v1 sinceef9b1dcec01(merged in fix(cli): prefer .kilo config directory #11638).paths.ts:19,31). Outside version control it still reaches the filesystem root. The comparison resolves symlinks, so a/tmpspelling of a/private/tmprepository also stops.kilo.jsonc,kilo.json,opencode.jsonc,opencode.json, else to a newkilo.jsonc(globalConfigFile,config.ts:206-216).kilo mcp add(also used byplugin addandplugin remove) prefers.kilo, then.kilocode, then root files, and createskilo.json(mcp.ts:396-426).Commands, agents, modes, skills and plugins need no change of their own. v2 loads them from every config
Directoryentry, so.kilo/command,.kilo/agent,.kilo/mode,.kilo/skillsand.kilo/pluginswork when the directories are discovered.The global config directory is not hardcoded. It comes from
Global(global.config), so it follows the app name when the identity work changes it.Shape of the change:
packages/core/src/kilocode/config/discovery.ts.kilocode_change:config/discovery.ts,config.ts(directory file order parameter and writer) andcli/.../mcp/add.ts.packages/core/src/plugin/internal.tsis not touched.Two deliberate differences from upstream v2: the walk stops at the worktree root, and
.opencodeis not read. Both follow v1.User Impact
kilo.jsonandkilo.jsoncload from the global config directory, the project and parent directories up to the worktree root, and from.kiloand.kilocode..kilo/and.kilocode/are available, with.kilowinning on conflicts..opencode/content and config above the repository root no longer load.kilo.jsonc.kilo mcp addcreateskilo.json.Evidence
Typecheck
packages/corebun typecheckpackages/clibun typecheckpackages/plugin/src/source.node.ts(registerHooks, 4 errors). Same errors on the unmodified treeTests
All runs used temporary
XDG_DATA_HOME,XDG_CONFIG_HOME,XDG_STATE_HOMEandXDG_CACHE_HOME. A check after the runs found no new files in the real OpenCode directories.bun run test test/kilocode(core, new)test/kilocode test/config test/formatter.test.ts test/plugin/supervisor-reload.test.ts test/plugin/supervisor.test.ts test/session-activation.test.ts test/skill.test.ts test/agent.test.ts test/command.test.ts test/instruction-discovery.test.ts test/preload.test.tsbun run testbun run testbun run testEvery failure also happens on the unmodified tree. I checked this by running the same files with the changed files restored to the base:
test/plugin/supervisor-reload.test.ts(RPC is unavailable: greeter, one 15 s timeout).Session.shelltests. The baseline lists 7 macOS bash 3.2 shell failures.test/plugin/module.test.ts.test/worktree.test.ts(canonical plugins).fs.test.tstimeout passed on rerun (flaky).The baseline for the shell failures is
pinned-v2-baseline.mdonkilo-v2-clean-bootstrap. Package code is identical between that base andv2.Upstream tests changed, with
kilocode_changemarkers. None is skipped or deleted..opencodefixtures became.kilo:config/config.test.ts,config/reload.test.ts,config/command-subagent.test.ts,formatter.test.ts,plugin/supervisor-reload.test.tsandsession-activation.test.ts.config.test.ts"loads global and ancestor configuration across the project boundary" became "... up to the project boundary". The file above the git root (outside) is no longer expected.packages/cli/script/service-smoke.tscreates its plugin in.kilo/plugins. The first CI run failedVerify compiled service lifecyclewithCompiled service did not discover the created plugin, because the smoke used.opencode/plugins. The compiled build cannot run locally (it requires bun^1.4.2, this machine has1.4.0-canary), so CI verifies this step.Self-test (real CLI,
bun devisolated profile, TUI harness from the v2 migration skill, headless)The fixture project is a git repo. The TUI was started in the nested folder
repo/app. Each config file defines a slash command with the same name and a description that names the file, so the slash list shows which file wins.kilo.jsondefault_agent: planPlan/rootorderin rootkilo.jsonandkilo.jsoncroot kilo.jsonc/depthin rootkilo.json, rootopencode.json,app/kilo.jsonroot opencode.json/dirorderin.kilo/kilo.jsoncand.kilo/kilo.json.kilo/kilo.json/legacyin.kilocode/kilo.jsonand.kilo/kilo.json.kilo/kilo.json/globalorderin globalkilo.jsonandkilo.jsoncglobal kilo.jsonc.kilo/command/kilo-hello.md,.kilocode/command/legacy-hello.md.opencode/command/opencode-hello.mdkilo.jsonand.kilo/commandabove the git root/kilo-helloKILOOKkilo/kilo-auto/free/kilo-hellowithkilo/deepseek/deepseek-v4.1-flashYou need to sign in to use this modelin the dev profile (Gateway and login work, #14998). Config loading was already confirmed by the sent templatekilo mcp addin a new project, then with.kilo/kilo.jsoncpresent, then--globalkilo.json,.kilo/kilo.jsonc, globalkilo.jsonThe CLI logs have no errors except the sign-in error above. The headless run cannot take screenshots, so the evidence is the terminal text.
Follow-ups (not in this PR)
.kiloand.kilocode(separate PR, ready locally)./Library/Application Support/kilo,%ProgramData%\kiloand/etc/kiloas the highest priority, plus the macOS MDM plistai.opencode.managed. v2 has no managed config directory, andManagedPolicyonly holds Console statements. Policy ranking also reverses document order, so a managed document placed last would rank lowest for policies. The v2 path is an open question.memory_model, swarm, PTC, cleanup)..kiloand.kilocodeof the main checkout from a git worktree.~/.kiloand~/.kilocode. Not done here because v2 uses separatekilo2directories..opencodenotice from v1.packages/core/script/test.tsandpackages/core/test/preload.test.tshardcodeopencodepaths and will need changes when the app name changes.KILO_*env names, and the updater, which reads onlyopencode.*from global config.Review follow-ups
The Kilo Code Review on this PR raised three suggestions. One is fixed here, two are recorded for the config unit:
resolveConfigPathtakes aglobalflag again, somcp add --globalandplugin add/removeonly probe root files in the global directory, like v1. Without it a server or plugin could be written into.kilo/or.kilocode/under the global directory, which the config layer never reads.config.json(lowest read priority in v1, last write candidate, written by the v1 TOML migration) is not read or written here. It needs a global-only entry in the read chain and inConfig.update; it cannot join the sharednameslist because project roots must not read it..kilooverride a nested one, while this PR keeps the nearest.kilowinning (the existing.toReversed()). That matches root-file behavior and v2 conventions; a cross-level test and a decision should pin it.