Merge branch 'main' into session/agent_9864f1dc-ca7c-4243-877b-14d089b66f92

This commit is contained in:
Christiaan Arnoldus
2026-04-15 11:49:58 +02:00
committed by GitHub
44 changed files with 1068 additions and 61518 deletions
+11 -3
View File
@@ -15,8 +15,9 @@ import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { Permission } from "@/permission"
import { mergeDeep, pipe, sortBy, values } from "remeda"
import { Global } from "@/global"
import path from "path"
import { Global } from "@/global" // kilocode_change
import { KilocodePaths } from "@/kilocode/paths" // kilocode_change
import path from "path" // kilocode_change
import { Plugin } from "@/plugin"
import { Skill } from "../skill"
import { Effect, ServiceMap, Layer } from "effect"
@@ -84,7 +85,14 @@ export namespace Agent {
Effect.fn("Agent.state")(function* (ctx) {
const cfg = yield* config.get()
const skillDirs = yield* skill.dirs()
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
// kilocode_change start - include global config dirs so agents can read them without prompting
const whitelistedDirs = [
Truncate.GLOB,
...skillDirs.map((dir) => path.join(dir, "*")),
path.join(Global.Path.config, "*"),
...KilocodePaths.globalDirs().map((dir) => path.join(dir, "*")),
]
// kilocode_change end
const baseDefaults = Permission.fromConfig({
// kilocode_change: renamed from defaults
+1
View File
@@ -8,3 +8,4 @@ export const GlobalBus = new EventEmitter<{
},
]
}>()
GlobalBus.setMaxListeners(50) // kilocode_change — surface warning if SSE listeners accumulate
@@ -0,0 +1,158 @@
// kilocode_change - new file
import path from "path"
import { type ParseError, parse, printParseErrorCode } from "jsonc-parser"
import { ConfigProtection } from "./permission/config-paths"
import { ConfigMarkdown } from "@/config/markdown"
import { Config } from "@/config/config"
import { ConfigPaths } from "@/config/paths"
import { Instance } from "@/project/instance"
export namespace ConfigValidation {
const JSONC_EXT = new Set([".json", ".jsonc"])
const COMMAND_DIRS = new Set(["command", "commands"])
const AGENT_DIRS = new Set(["agent", "agents"])
const MODE_DIRS = new Set(["mode", "modes"])
function label(filepath: string): string {
const rel = path.isAbsolute(filepath)
? filepath.replace(process.env.HOME || "~", "~")
: filepath
return rel
}
async function jsonc(filepath: string): Promise<string> {
const text = await ConfigPaths.readFile(filepath)
if (text === undefined) return ""
const errors: ParseError[] = []
const data = parse(text, errors, { allowTrailingComma: true })
if (errors.length > 0) {
const lines = text.split("\n")
const detail = errors
.map((e) => {
const before = text.substring(0, e.offset).split("\n")
const line = before.length
const col = before[before.length - 1].length + 1
const src = lines[line - 1]
const msg = `${printParseErrorCode(e.error)} at line ${line}, column ${col}`
return src ? `${msg}\n Line ${line}: ${src}` : msg
})
.join("\n")
return `\n\n<config_validation>\nERROR: Config file at ${label(filepath)} is not valid JSON(C)\n ${detail}\n</config_validation>`
}
const result = Config.Info.safeParse(data)
if (!result.success) {
const issues = result.error.issues
.map((i) => ` ${i.path.join(".")}: ${i.message}`)
.join("\n")
return `\n\n<config_validation>\nWARNING: Configuration is invalid at ${label(filepath)}\n${issues}\n</config_validation>`
}
return `\n\n<config_validation>\nConfig file validated successfully.\n</config_validation>`
}
async function markdown(filepath: string): Promise<string> {
const dir = path.basename(path.dirname(filepath))
// Determine schema from parent directory
const schema = COMMAND_DIRS.has(dir)
? "command"
: AGENT_DIRS.has(dir) || MODE_DIRS.has(dir)
? "agent"
: undefined
if (!schema) return ""
let md: Awaited<ReturnType<typeof ConfigMarkdown.parse>>
try {
md = await ConfigMarkdown.parse(filepath)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
const msg = ConfigMarkdown.FrontmatterError.isInstance(e)
? e.data.message
: `Failed to parse frontmatter: ${e instanceof Error ? e.message : String(e)}`
return `\n\n<config_validation>\nERROR: ${label(filepath)}\n ${msg}\n</config_validation>`
}
const config = schema === "command"
? { ...md.data, template: md.content.trim() }
: { ...md.data, prompt: md.content.trim() }
const zod = schema === "command" ? Config.Command : Config.Agent
const result = zod.safeParse(config)
if (!result.success) {
const issues = result.error.issues
.map((i) => ` ${i.path.join(".")}: ${i.message}`)
.join("\n")
return `\n\n<config_validation>\nWARNING: Configuration is invalid at ${label(filepath)}\n${issues}\n</config_validation>`
}
return `\n\n<config_validation>\nConfig file validated successfully.\n</config_validation>`
}
function isConfig(filepath: string): boolean {
if (!path.isAbsolute(filepath)) return ConfigProtection.isRelative(filepath)
// Global config dirs (e.g. ~/.config/kilo/)
if (ConfigProtection.isAbsolute(filepath)) return true
// Project-local config (e.g. /project/.kilo/command/foo.md)
try {
const rel = path.relative(Instance.worktree, filepath)
if (!rel.startsWith("..")) return ConfigProtection.isRelative(rel)
} catch {
// Not in an Instance context — skip project-relative check
}
return false
}
async function existing(): Promise<string> {
try {
const warns = await Config.warnings()
if (!warns || warns.length === 0) return ""
const items = warns
.map((w: Config.Warning) => ` ${label(w.path)}: ${w.message}`)
.join("\n")
return `Pre-existing config issues (from session start):\n${items}\n\n`
} catch {
return ""
}
}
/**
* Validate a file if it's a config file. Returns formatted output to append
* to tool results, or empty string for non-config files.
*/
export async function check(filepath: string): Promise<string> {
if (!isConfig(filepath)) return ""
const ext = path.extname(filepath).toLowerCase()
// Skip AGENTS.md and other root .md files not in a recognized config subdir
if (ext === ".md") {
const dir = path.basename(path.dirname(filepath))
if (!COMMAND_DIRS.has(dir) && !AGENT_DIRS.has(dir) && !MODE_DIRS.has(dir)) {
return ""
}
}
const prefix = await existing()
const validation = JSONC_EXT.has(ext)
? await jsonc(filepath)
: ext === ".md"
? await markdown(filepath)
: ""
if (!validation) return ""
if (prefix) {
// Replace the opening tag content with prefixed version
return validation.replace(
"<config_validation>\n",
`<config_validation>\n${prefix}Post-edit validation of ${label(filepath)}:\n`,
)
}
return validation
}
}
@@ -80,18 +80,21 @@ export namespace ConfigProtection {
/**
* Determine if a permission request targets config files.
* Checks `edit` and `external_directory` permissions — read access is not restricted.
* Gates `edit` permissions and bash-originated `external_directory` requests.
* File-tool reads are not restricted.
*/
export function isRequest(request: {
permission: string
patterns: string[]
metadata?: Record<string, any>
}): boolean {
// external_directory patterns are absolute globs like "/Users/alex/.config/kilo/*"
if (request.permission === "external_directory") {
// File tools include metadata.filepath. They may read global config
// without prompting, but edits are still protected separately via `edit`.
if (request.metadata?.filepath) return false
for (const pattern of request.patterns) {
const dir = pattern.replace(/\/\*$/, "")
if (isAbsolute(dir)) return true
if (path.isAbsolute(dir) && isAbsolute(dir)) return true
}
return false
}
@@ -1,6 +1,5 @@
// kilocode_change - new file
import { $ } from "bun"
import { createTwoFilesPatch } from "diff"
import fs from "node:fs/promises"
import path from "node:path"
import z from "zod"
@@ -245,7 +244,7 @@ export namespace WorktreeDiff {
const additions = meta.status === "added" && meta.additions === 0 && !meta.tracked ? lines(after) : meta.additions
return {
file: meta.file,
patch: createTwoFilesPatch(meta.file, meta.file, before, after),
patch: "",
before,
after,
additions,
-2
View File
@@ -1,2 +0,0 @@
// Auto-generated by build.ts - do not edit
export declare const snapshot: Record<string, unknown>
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -70,14 +70,27 @@ export const EventRoutes = () =>
stream.onAbort(stop)
// kilocode_change start
// On Windows, stream.onAbort() may never fire after a client disconnects
// (delayed TCP RST detection via IOCP). Without this try/catch, the
// GlobalBus listener, heartbeat interval, and AsyncQueue stay alive
// indefinitely for each dead connection — leaking memory on every
// SSE reconnect. Catching write errors lets us clean up eagerly.
try {
for await (const data of q) {
if (data === null) return
await stream.writeSSE({ data })
try {
await stream.writeSSE({ data })
} catch {
log.info("event write failed, cleaning up dead stream")
stop()
return
}
}
} finally {
stop()
}
// kilocode_change end
})
},
)
+14 -1
View File
@@ -56,14 +56,27 @@ async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>
stream.onAbort(stop)
// kilocode_change start
// On Windows, stream.onAbort() may never fire after a client disconnects
// (delayed TCP RST detection via IOCP). Without this try/catch, the
// GlobalBus listener, heartbeat interval, and AsyncQueue stay alive
// indefinitely for each dead connection — leaking memory on every
// SSE reconnect. Catching write errors lets us clean up eagerly.
try {
for await (const data of q) {
if (data === null) return
await stream.writeSSE({ data })
try {
await stream.writeSSE({ data })
} catch {
log.info("global event write failed, cleaning up dead stream")
stop()
return
}
}
} finally {
stop()
}
// kilocode_change end
})
}
+10
View File
@@ -148,6 +148,16 @@ export namespace Skill {
directory: string,
worktree: string,
) {
// kilocode_change start - seed built-in skills before discovery so user skills can override
for (const skill of BUILTIN_SKILLS) {
state.skills[skill.name] = {
name: skill.name,
description: skill.description,
location: BUILTIN_LOCATION,
content: skill.content,
}
}
// kilocode_change end
if (!Flag.KILO_DISABLE_EXTERNAL_SKILLS) {
for (const dir of EXTERNAL_DIRS) {
const root = path.join(Global.Path.home, dir)
@@ -14,6 +14,7 @@ import { Filesystem } from "../util/filesystem"
import DESCRIPTION from "./apply_patch.txt"
import { File } from "../file"
import { filterDiagnostics } from "./diagnostics" // kilocode_change
import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change
import { Format } from "../format"
const PatchParams = z.object({
@@ -271,6 +272,10 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
const changedPaths = fileChanges
.filter((c) => c.type !== "delete")
.map((c) => Filesystem.normalizePath(c.movePath ?? c.filePath))
for (const changed of fileChanges) {
if (changed.type === "delete") continue
output += await ConfigValidation.check(changed.movePath ?? changed.filePath)
}
// kilocode_change end
return {
+2
View File
@@ -19,6 +19,7 @@ import { Instance } from "../project/instance"
import { Snapshot } from "@/snapshot"
import { assertExternalDirectory } from "./external-directory"
import { filterDiagnostics } from "./diagnostics" // kilocode_change
import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change
const MAX_DIAGNOSTICS_PER_FILE = 20
const MAX_DIFF_CONTENT = 500_000 // kilocode_change
@@ -180,6 +181,7 @@ export const EditTool = Tool.define("edit", {
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
output += `\n\nLSP errors detected in this file, please fix:\n<diagnostics file="${filePath}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
}
output += await ConfigValidation.check(filePath) // kilocode_change
return {
metadata: {
+21 -2
View File
@@ -7,6 +7,24 @@ import { Ripgrep } from "../file/ripgrep"
import { Instance } from "../project/instance"
import { assertExternalDirectory } from "./external-directory"
// kilocode_change start — support absolute glob patterns (e.g. ~/.config/kilo/command/*.md)
function normalize(p: string) {
return p.replaceAll("\\", "/")
}
function split(pattern: string) {
const normalized = normalize(pattern)
if (!path.isAbsolute(normalized)) return
const index = normalized.search(/[*?{[]/)
if (index === -1) return { dir: normalized, pattern: "*" }
const slice = normalized.slice(0, index)
const cut = slice.lastIndexOf("/")
const dir = cut > 0 ? slice.slice(0, cut) : "/"
const next = normalized.slice(cut + 1)
return { dir, pattern: next || "*" }
}
// kilocode_change end
export const GlobTool = Tool.define("glob", {
description: DESCRIPTION,
parameters: z.object({
@@ -19,6 +37,7 @@ export const GlobTool = Tool.define("glob", {
),
}),
async execute(params, ctx) {
const absolute = split(params.pattern) // kilocode_change
await ctx.ask({
permission: "glob",
patterns: [params.pattern],
@@ -29,7 +48,7 @@ export const GlobTool = Tool.define("glob", {
},
})
let search = params.path ?? Instance.directory
let search = absolute?.dir ?? params.path ?? Instance.directory // kilocode_change
search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search)
await assertExternalDirectory(ctx, search, { kind: "directory" })
@@ -38,7 +57,7 @@ export const GlobTool = Tool.define("glob", {
let truncated = false
for await (const file of Ripgrep.files({
cwd: search,
glob: [params.pattern],
glob: [absolute?.pattern ?? params.pattern], // kilocode_change
signal: ctx.abort,
})) {
if (files.length >= limit) {
+2
View File
@@ -14,6 +14,7 @@ import { Instance } from "../project/instance"
import { trimDiff, buildFileDiff } from "./edit" // kilocode_change
import { assertExternalDirectory } from "./external-directory"
import { filterDiagnostics } from "./diagnostics" // kilocode_change
import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change
const MAX_DIAGNOSTICS_PER_FILE = 20
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
@@ -73,6 +74,7 @@ export const WriteTool = Tool.define("write", {
projectDiagnosticsCount++
output += `\n\nLSP errors detected in other files:\n<diagnostics file="${file}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
}
output += await ConfigValidation.check(filepath) // kilocode_change
return {
title: path.relative(Instance.worktree, filepath),
@@ -0,0 +1,25 @@
// kilocode_change - new file
import { afterEach, test, expect } from "bun:test"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
import { Permission } from "../../src/permission"
import { Global } from "../../src/global"
afterEach(async () => {
await Instance.disposeAll()
})
test("code agent allows global config directory reads by default", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const code = await Agent.get("code")
expect(code).toBeDefined()
expect(Permission.evaluate("external_directory", `${Global.Path.config}/*`, code!.permission).action).toBe(
"allow",
)
},
})
})
@@ -0,0 +1,75 @@
import { afterEach, test, expect } from "bun:test"
import path from "path"
import { Skill } from "../../src/skill"
import { Instance } from "../../src/project/instance"
import { BUILTIN_SKILLS } from "../../src/kilocode/skills/builtin"
import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
await Instance.disposeAll()
})
test("built-in skills are present in empty project", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
for (const builtin of BUILTIN_SKILLS) {
const found = skills.find((s) => s.name === builtin.name)
expect(found).toBeDefined()
expect(found!.location).toBe(Skill.BUILTIN_LOCATION)
expect(found!.description).toBe(builtin.description)
expect(found!.content.length).toBeGreaterThan(0)
}
},
})
})
test("built-in skill has correct metadata", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skill = await Skill.get("kilo-config")
expect(skill).toBeDefined()
expect(skill!.name).toBe("kilo-config")
expect(skill!.location).toBe(Skill.BUILTIN_LOCATION)
expect(skill!.content).toContain("kilo")
},
})
})
test("user skill overrides built-in with same name", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const skillDir = path.join(dir, ".kilo", "skill", "kilo-config")
await Bun.write(
path.join(skillDir, "SKILL.md"),
`---
name: kilo-config
description: User override of kilo-config.
---
# Custom kilo-config
User-provided content.
`,
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skill = await Skill.get("kilo-config")
expect(skill).toBeDefined()
expect(skill!.description).toBe("User override of kilo-config.")
expect(skill!.location).not.toBe(Skill.BUILTIN_LOCATION)
expect(skill!.location).toContain(path.join("skill", "kilo-config", "SKILL.md"))
},
})
})
@@ -0,0 +1,184 @@
// kilocode_change - new file
import { afterEach, describe, expect, test } from "bun:test"
import path from "path"
import { ConfigValidation } from "../../src/kilocode/config-validation"
import { Instance } from "../../src/project/instance"
import { Config } from "../../src/config/config"
import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
await Instance.disposeAll()
})
describe("ConfigValidation.check", () => {
test("returns empty string for non-config files", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, "src", "index.ts")
await Filesystem.write(filepath, "export const x = 1")
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toBe("")
})
test("validates valid JSONC config", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, "kilo.json")
await Filesystem.write(filepath, JSON.stringify({ model: "anthropic/claude-sonnet-4-20250514" }))
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toContain("config_validation")
expect(result).toContain("validated successfully")
})
test("reports JSONC syntax errors", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, "kilo.json")
await Filesystem.write(filepath, '{ "model": "test/model" "extra": true }')
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toContain("config_validation")
expect(result).toContain("ERROR")
expect(result).toContain("not valid JSON(C)")
})
test("reports schema validation errors for unknown fields", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, "kilo.json")
// Config.Info uses .strict() so unknown fields produce errors
await Filesystem.write(filepath, JSON.stringify({ notAField: true }))
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toContain("config_validation")
expect(result).toContain("WARNING")
expect(result).toContain("invalid")
})
test("validates valid markdown command", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, ".kilo", "command", "test-cmd.md")
await Filesystem.write(
filepath,
`---
description: A test command
---
Do something useful`,
)
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toContain("config_validation")
expect(result).toContain("validated successfully")
})
test("reports schema error for command with invalid field types", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, ".kilo", "command", "bad.md")
// agent expects string but gets number — schema validation fails
await Filesystem.write(
filepath,
`---
agent: 123
subtask: "not-a-boolean"
---
Do something`,
)
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toContain("config_validation")
expect(result).toContain("WARNING")
expect(result).toContain("invalid")
})
test("validates valid markdown agent", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, ".kilo", "agent", "helper.md")
await Filesystem.write(
filepath,
`---
model: anthropic/claude-sonnet-4-20250514
description: A helper agent
---
You are a helpful agent.`,
)
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toContain("config_validation")
expect(result).toContain("validated successfully")
})
test("skips AGENTS.md (root md file not in config subdir)", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, "AGENTS.md")
await Filesystem.write(filepath, "# Project agents")
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toBe("")
})
test("skips plan files (excluded subdir)", async () => {
await using tmp = await tmpdir({ git: true })
const filepath = path.join(tmp.path, ".kilo", "plans", "plan.md")
await Filesystem.write(filepath, "# Plan")
const result = await Instance.provide({
directory: tmp.path,
fn: () => ConfigValidation.check(filepath),
})
expect(result).toBe("")
})
test("includes pre-existing warnings when present", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
// Create a broken agent config that produces a warning at session start
await Filesystem.write(
path.join(dir, ".kilo", "agent", "broken.md"),
`---
mode: "banana"
---
Broken agent`,
)
},
})
const filepath = path.join(tmp.path, "kilo.json")
await Filesystem.write(filepath, JSON.stringify({ model: "anthropic/claude-sonnet-4-20250514" }))
const result = await Instance.provide({
directory: tmp.path,
fn: async () => {
// Force config load to populate warnings
await Config.get()
return ConfigValidation.check(filepath)
},
})
expect(result).toContain("Pre-existing config issues")
expect(result).toContain("broken.md")
expect(result).toContain("Post-edit validation")
})
})
@@ -0,0 +1,175 @@
// kilocode_change - new file
import path from "path"
import { describe, expect, test } from "bun:test"
import { ConfigProtection } from "../../../src/kilocode/permission/config-paths"
import { Global } from "../../../src/global"
import { KilocodePaths } from "../../../src/kilocode/paths"
describe("ConfigProtection.isRequest", () => {
const config = path.resolve(Global.Path.config)
const legacy = KilocodePaths.globalDirs().map((d) => path.resolve(d))
// --- external_directory: bash-originated (empty metadata) ---
test("returns true for bash external_directory targeting global config", () => {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: [config + "/*"],
metadata: {},
})
expect(result).toBe(true)
})
test("returns true for bash external_directory targeting skill dir", () => {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: [path.join(config, "skills", "my-skill") + "/*"],
metadata: {},
})
expect(result).toBe(true)
})
test("returns true for bash external_directory targeting legacy global dir", () => {
for (const dir of legacy) {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: [dir + "/*"],
metadata: {},
})
expect(result).toBe(true)
}
})
// --- external_directory: file-tool-originated (has metadata.filepath) ---
test("returns false for file-tool external_directory targeting global config", () => {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: [config + "/*"],
metadata: { filepath: path.join(config, "kilo.json"), parentDir: config },
})
expect(result).toBe(false)
})
test("returns false for file-tool external_directory targeting global config root dir", () => {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: [config + "/*"],
metadata: { filepath: config, parentDir: config },
})
expect(result).toBe(false)
})
test("returns false for file-tool external_directory targeting readable global command dir", () => {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: [path.join(config, "command") + "/*"],
metadata: { filepath: path.join(config, "command", "foo.md"), parentDir: path.join(config, "command") },
})
expect(result).toBe(false)
})
test("returns false for file-tool external_directory targeting readable global skill dir", () => {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: [path.join(config, "skills") + "/*"],
metadata: {
filepath: path.join(config, "skills", "my-skill", "SKILL.md"),
parentDir: path.join(config, "skills"),
},
})
expect(result).toBe(false)
})
// --- external_directory: non-config dirs ---
test("returns false for bash external_directory targeting non-config dir", () => {
const result = ConfigProtection.isRequest({
permission: "external_directory",
patterns: ["/tmp/some-project/*"],
metadata: {},
})
expect(result).toBe(false)
})
// --- edit permission ---
test("returns true for edit targeting global config file via metadata.filepath", () => {
const result = ConfigProtection.isRequest({
permission: "edit",
patterns: [],
metadata: { filepath: path.join(config, "config.json") },
})
expect(result).toBe(true)
})
test("returns true for edit targeting skill file via metadata.filepath", () => {
const result = ConfigProtection.isRequest({
permission: "edit",
patterns: [],
metadata: { filepath: path.join(config, "skills", "my-skill", "SKILL.md") },
})
expect(result).toBe(true)
})
test("returns true for edit targeting legacy global dir via metadata.filepath", () => {
for (const dir of legacy) {
const result = ConfigProtection.isRequest({
permission: "edit",
patterns: [],
metadata: { filepath: path.join(dir, "config.json") },
})
expect(result).toBe(true)
}
})
test("returns true for edit targeting relative config path via patterns", () => {
const result = ConfigProtection.isRequest({
permission: "edit",
patterns: [".kilo/command/foo.md"],
})
expect(result).toBe(true)
})
test("returns false for edit targeting excluded subdir (plans)", () => {
const result = ConfigProtection.isRequest({
permission: "edit",
patterns: [".kilo/plans/plan.md"],
})
expect(result).toBe(false)
})
test("returns false for read permission", () => {
const result = ConfigProtection.isRequest({
permission: "read",
patterns: [".kilo/config.json"],
})
expect(result).toBe(false)
})
test("returns false for bash permission", () => {
const result = ConfigProtection.isRequest({
permission: "bash",
patterns: ["cat " + path.join(config, "config.json")],
})
expect(result).toBe(false)
})
test("returns true for edit targeting root config files", () => {
for (const file of ["kilo.json", "kilo.jsonc", "AGENTS.md"]) {
const result = ConfigProtection.isRequest({
permission: "edit",
patterns: [file],
})
expect(result).toBe(true)
}
})
test("returns false for edit targeting non-config files", () => {
const result = ConfigProtection.isRequest({
permission: "edit",
patterns: ["src/index.ts"],
})
expect(result).toBe(false)
})
})
@@ -1,6 +1,7 @@
import { test, expect, describe } from "bun:test"
import { $ } from "bun"
import { tmpdir } from "../fixture/fixture"
import { WorktreeDiff } from "../../src/kilocode/review/worktree-diff"
import path from "path"
/**
@@ -145,6 +146,19 @@ describe("worktree diff git commands", () => {
expect(allFiles).toContain("new-file.py")
})
test("worktree detail does not include unused patch content", async () => {
await using tmp = await setupRepo()
const dir = tmp.path
await Bun.write(path.join(dir, "existing.txt"), "hello\nmodified\n")
const detail = await WorktreeDiff.detail({ dir, base: "HEAD", file: "existing.txt" })
expect(detail?.patch).toBe("")
expect(detail?.before).toBe("hello\n")
expect(detail?.after).toBe("hello\nmodified\n")
})
test("worktree scenario: branch with no new commits, only untracked files", async () => {
// This is the exact scenario from the screenshot:
// - Worktree created from main
+9 -9
View File
@@ -51,7 +51,7 @@ Instructions here.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change
expect(skills.length).toBe(1)
const testSkill = skills.find((s) => s.name === "test-skill")
expect(testSkill).toBeDefined()
@@ -131,7 +131,7 @@ description: Second test skill.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change
expect(skills.length).toBe(2)
expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
@@ -157,7 +157,7 @@ Just some content without YAML frontmatter.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change
expect(skills).toEqual([])
},
})
@@ -184,7 +184,7 @@ description: A skill in the .claude/skills directory.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change
expect(skills.length).toBe(1)
const claudeSkill = skills.find((s) => s.name === "claude-skill")
expect(claudeSkill).toBeDefined()
@@ -204,7 +204,7 @@ test("discovers global skills from ~/.claude/skills/ directory", async () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change // kilocode_change
expect(skills.length).toBe(1)
expect(skills[0].name).toBe("global-test-skill")
expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
@@ -222,7 +222,7 @@ test("returns empty array when no skills exist", async () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change
expect(skills).toEqual([])
},
})
@@ -249,7 +249,7 @@ description: A skill in the .agents/skills directory.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change
expect(skills.length).toBe(1)
const agentSkill = skills.find((s) => s.name === "agent-skill")
expect(agentSkill).toBeDefined()
@@ -283,7 +283,7 @@ This skill is loaded from the global home directory.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change // kilocode_change
expect(skills.length).toBe(1)
expect(skills[0].name).toBe("global-agent-skill")
expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.")
@@ -327,7 +327,7 @@ description: A skill in the .agents/skills directory.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((s) => s.location !== Skill.BUILTIN_LOCATION) // kilocode_change
expect(skills.length).toBe(2)
expect(skills.find((s) => s.name === "claude-skill")).toBeDefined()
expect(skills.find((s) => s.name === "agent-skill")).toBeDefined()
+47
View File
@@ -0,0 +1,47 @@
// kilocode_change - new file
import { describe, expect, test } from "bun:test"
import path from "path"
import { GlobTool } from "../../src/tool/glob"
import { Instance } from "../../src/project/instance"
import { SessionID, MessageID } from "../../src/session/schema"
import { tmpdir } from "../fixture/fixture"
describe("tool.glob", () => {
const ctx = {
sessionID: SessionID.make("test"),
messageID: MessageID.make("test"),
callID: "",
agent: "code",
abort: AbortSignal.any([]),
messages: [],
metadata: () => {},
ask: async () => {},
}
test("supports absolute glob patterns outside the project", async () => {
await using outer = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "one.md"), "one")
await Bun.write(path.join(dir, "two.md"), "two")
await Bun.write(path.join(dir, "three.txt"), "three")
},
})
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const glob = await GlobTool.init()
const result = await glob.execute(
{
pattern: path.join(outer.path, "*.md"),
},
ctx,
)
expect(result.output).toContain(path.join(outer.path, "one.md"))
expect(result.output).toContain(path.join(outer.path, "two.md"))
expect(result.output).not.toContain(path.join(outer.path, "three.txt"))
},
})
})
})