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
5 changes: 5 additions & 0 deletions .changeset/board-env-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Enable the experimental shared agent board (Kilo Swarm) with the `KILO_EXPERIMENTAL_SHARED_AGENT_BOARD` environment variable, or the umbrella `KILO_EXPERIMENTAL`, in addition to the `experimental.shared_agent_board` config key.
4 changes: 2 additions & 2 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const layer = Layer.effect(
})

// kilocode_change start - patch defaults with bash allowlist and recall permission
const kilo = KiloAgent.prepare(cfg)
const kilo = KiloAgent.prepare(cfg, flags)
const defaults = Permission.merge(baseDefaults, kilo.defaultsPatch)
// kilocode_change end

Expand Down Expand Up @@ -325,7 +325,7 @@ const layer = Layer.effect(
}

// kilocode_change start - rename build→code, add debug/orchestrator/ask, patch plan/explore
KiloAgent.patchAgents(agents, defaults, user, cfg, kilo, ctx.worktree, whitelistedDirs)
KiloAgent.patchAgents(agents, defaults, user, kilo, ctx.worktree, whitelistedDirs)

const agentConfigs = KiloAgent.preprocessConfig(cfg.agent ?? {})
for (const [key, value] of Object.entries(agentConfigs)) {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/effect/runtime-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
experimentalCodeMode: enabledByExperimental("KILO_EXPERIMENTAL_CODE_MODE"),
experimentalEventSystem: enabledByExperimental("KILO_EXPERIMENTAL_EVENT_SYSTEM"),
experimentalSessionSwitcher: enabledByExperimental("KILO_EXPERIMENTAL_SESSION_SWITCHER"), // kilocode_change
experimentalSharedAgentBoard: enabledByExperimental("KILO_EXPERIMENTAL_SHARED_AGENT_BOARD"), // kilocode_change
experimentalWorkspaces: enabledByExperimental("KILO_EXPERIMENTAL_WORKSPACES"),
experimentalIconDiscovery: enabledByExperimental("KILO_EXPERIMENTAL_ICON_DISCOVERY"),
experimentalMcpApps: enabledByExperimental("KILO_EXPERIMENTAL_MCP_APPS"), // kilocode_change
Expand Down
16 changes: 11 additions & 5 deletions packages/opencode/src/kilocode/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import path from "path"
import { Global } from "@opencode-ai/core/global"
import { Flag } from "@opencode-ai/core/flag/flag"
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser"
import type { RuntimeFlags } from "@/effect/runtime-flags"
import { BoardEnabled } from "@/kilocode/board/enabled"
import { KilocodeConfigSources } from "../config/sources"

import PROMPT_DEBUG from "../../agent/prompt/debug.txt"
Expand Down Expand Up @@ -356,14 +358,19 @@ export function getMcpRules(cfg: Config.Info): Record<string, "allow" | "ask" |
export interface KiloData {
mcpRules: Record<string, "allow" | "ask" | "deny">
defaultsPatch: Permission.Ruleset
board: boolean
}

// Prepare kilo-specific data derived from config. Call once per state initialization.
export function prepare(cfg: Config.Info): KiloData {
export function prepare(cfg: Config.Info, flags: Pick<RuntimeFlags.Info, "experimentalSharedAgentBoard">): KiloData {
const mcpRules = getMcpRules(cfg)
const enabled = BoardEnabled.resolve({
config: cfg.experimental?.shared_agent_board,
flag: flags.experimentalSharedAgentBoard,
})
const defaultsPatch = Permission.fromConfig({
bash,
...board(cfg.experimental?.shared_agent_board === true),
...board(enabled),
recall: "ask",
...(Flag.KILO_CLIENT === "vscode" && cfg.experimental?.native_notebook_tools === true
? { notebook_read: "ask" as const, notebook_edit: "ask" as const, notebook_execute: "ask" as const }
Expand All @@ -372,7 +379,7 @@ export function prepare(cfg: Config.Info): KiloData {
kilo_memory_recall: "ask",
kilo_memory_save: "ask",
})
return { mcpRules, defaultsPatch }
return { mcpRules, defaultsPatch, board: enabled }
}

export function cacheKey(cfg: Config.Info) {
Expand Down Expand Up @@ -489,12 +496,11 @@ export function patchAgents(
>,
defaults: Permission.Ruleset,
user: Permission.Ruleset,
cfg: Config.Info,
kilo: KiloData,
worktree: string,
whitelistedDirs: string[],
) {
const enabled = cfg.experimental?.shared_agent_board === true
const enabled = kilo.board
// Rename "build" → "code" for backward compatibility
if (agents.build) {
agents.code = {
Expand Down
12 changes: 11 additions & 1 deletion packages/opencode/src/kilocode/board/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Cause, Effect } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { Config } from "@/config/config"
import { Permission } from "@/permission"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Agent } from "@/agent/agent"
import { Session } from "@/session/session"
import type { MessageV2 } from "@/session/message-v2"
import type { Tool } from "@/tool/tool"
import { KiloSessionPrompt } from "@/kilocode/session/prompt"
import { BoardEnabled } from "./enabled"
import { BoardStore } from "./store"
import { BoardNotice } from "./notice"

Expand Down Expand Up @@ -48,6 +50,7 @@ export namespace BoardContext {

export const notifier = Effect.fn("BoardContext.notifier")(function* (input: Input) {
const config = yield* Config.Service
const flags = yield* RuntimeFlags.Service
const sessions = yield* Session.Service
const agents = yield* Agent.Service
const database = yield* Database.Service
Expand All @@ -56,7 +59,14 @@ export namespace BoardContext {
Effect.gen(function* () {
if (signal?.aborted) return output
const cfg = yield* config.get()
if (cfg.experimental?.shared_agent_board !== true) return output
if (
!BoardEnabled.resolve({
config: cfg.experimental?.shared_agent_board,
flag: flags.experimentalSharedAgentBoard,
})
) {
return output
}
const session = yield* sessions.get(input.session.id)
const agent = yield* agents.get(input.agent.name, cfg)
if (!agent || !allowed({ session, agent, user: input.user })) return output
Expand Down
13 changes: 13 additions & 0 deletions packages/opencode/src/kilocode/board/enabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export namespace BoardEnabled {
/**
* Resolve the effective shared agent board state.
*
* The `experimental.shared_agent_board` config key is the source of truth for
* an explicit enable. The experimental environment flag is an additional
* enable path, so an explicit config `false` does not turn the board off when
* the flag is set.
*/
export function resolve(input: { config?: boolean; flag?: boolean }) {
return input.config === true || input.flag === true
}
}
22 changes: 18 additions & 4 deletions packages/opencode/src/kilocode/tool/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Config } from "@/config/config"
import { BackgroundJob } from "@/background/job"
import { SessionStatus } from "@/session/status"
import { Tool } from "@/tool/tool"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { BoardEnabled } from "@/kilocode/board/enabled"
import { BoardStore } from "@/kilocode/board/store"

const Read = Schema.Struct({
Expand Down Expand Up @@ -66,12 +68,13 @@ const snapshot = Effect.fn("BoardTools.snapshot")(function* (
export const BoardReadTool = Tool.define<
typeof Read,
ReadMeta,
Config.Service | Database.Service | BackgroundJob.Service | SessionStatus.Service,
Config.Service | Database.Service | BackgroundJob.Service | SessionStatus.Service | RuntimeFlags.Service,
"board_read"
>(
"board_read",
Effect.gen(function* () {
const config = yield* Config.Service
const flags = yield* RuntimeFlags.Service
const database = yield* Database.Service
const jobs = yield* BackgroundJob.Service
const status = yield* SessionStatus.Service
Expand All @@ -91,7 +94,12 @@ export const BoardReadTool = Tool.define<
execute: (params, ctx) =>
Effect.gen(function* () {
const cfg = yield* config.get()
if (cfg.experimental?.shared_agent_board !== true) {
if (
!BoardEnabled.resolve({
config: cfg.experimental?.shared_agent_board,
flag: flags.experimentalSharedAgentBoard,
})
) {
return yield* Effect.fail(
new Error("The shared agent board is disabled. Enable it in Experimental settings."),
)
Expand Down Expand Up @@ -123,12 +131,13 @@ export const BoardReadTool = Tool.define<
export const BoardPostTool = Tool.define<
typeof Post,
PostMeta,
Config.Service | Database.Service | BackgroundJob.Service | SessionStatus.Service,
Config.Service | Database.Service | BackgroundJob.Service | SessionStatus.Service | RuntimeFlags.Service,
"board_post"
>(
"board_post",
Effect.gen(function* () {
const config = yield* Config.Service
const flags = yield* RuntimeFlags.Service
const database = yield* Database.Service
const jobs = yield* BackgroundJob.Service
const status = yield* SessionStatus.Service
Expand All @@ -154,7 +163,12 @@ export const BoardPostTool = Tool.define<
execute: (params, ctx) =>
Effect.gen(function* () {
const cfg = yield* config.get()
if (cfg.experimental?.shared_agent_board !== true) {
if (
!BoardEnabled.resolve({
config: cfg.experimental?.shared_agent_board,
flag: flags.experimentalSharedAgentBoard,
})
) {
return yield* Effect.fail(
new Error("The shared agent board is disabled. Enable it in Experimental settings."),
)
Expand Down
11 changes: 8 additions & 3 deletions packages/opencode/src/kilocode/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { AgentManager, HostError } from "@/kilocode/agent-manager/service"
import { KiloSessions } from "@/kilo-sessions/kilo-sessions"
import * as Log from "@opencode-ai/core/util/log"
import type { Config } from "@/config/config"
import type { RuntimeFlags } from "@/effect/runtime-flags"
import { BoardEnabled } from "@/kilocode/board/enabled"
import { Agent } from "@/agent/agent"
import * as Truncate from "@/tool/truncate"
import { InstanceState } from "@/effect/instance-state"
Expand Down Expand Up @@ -275,13 +277,16 @@ export namespace KiloToolRegistry {
shared_agent_board?: boolean
}
},
flags: Pick<RuntimeFlags.Info, "experimentalSharedAgentBoard">,
): Tool.Def[] {
const enabled = BoardEnabled.resolve({
config: cfg.experimental?.shared_agent_board,
flag: flags.experimentalSharedAgentBoard,
})
return [
...(tools.goalReport ? [tools.goalReport] : []),
...(cfg.experimental?.image_generation === true ? [tools.image] : []),
...(cfg.experimental?.shared_agent_board === true && tools.boardRead && tools.boardPost
? [tools.boardRead, tools.boardPost]
: []),
...(enabled && tools.boardRead && tools.boardPost ? [tools.boardRead, tools.boardPost] : []),
...(tools.semantic ? [tools.semantic] : []),
tools.memory,
tools.save,
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,7 @@ export const layer = Layer.effect(
Effect.provideService(Database.Service, database),
Effect.provideService(Agent.Service, agents),
Effect.provideService(Session.Service, sessions),
Effect.provideService(RuntimeFlags.Service, flags),
)
: undefined
// kilocode_change end
Expand Down
10 changes: 8 additions & 2 deletions packages/opencode/src/session/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { Config } from "@/config/config"
import { PermissionProvenance } from "@/kilocode/permission/provenance"
import { McpApps } from "@/kilocode/mcp/apps"
import { BoardEnabled } from "@/kilocode/board/enabled"
// kilocode_change end
import { isRecord } from "@/util/record"
import { RuntimeFlags } from "@/effect/runtime-flags"
Expand Down Expand Up @@ -74,9 +75,15 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
const truncate = yield* Truncate.Service
// kilocode_change start - permission provenance
const config = yield* Config.Service
const flags = yield* RuntimeFlags.Service
const cfg = yield* config.get()
const permissionOrigins = cfg.permission_origins
const notify = cfg.experimental?.shared_agent_board === true ? input.notify : undefined
const notify = BoardEnabled.resolve({
config: cfg.experimental?.shared_agent_board,
flag: flags.experimentalSharedAgentBoard,
})
? input.notify
: undefined
type Output = Parameters<SessionProcessor.Handle["completeToolCall"]>[1]
const finish = <T extends Output>(name: string, output: T, opts: ToolExecutionOptions) =>
Effect.gen(function* () {
Expand All @@ -91,7 +98,6 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
return result
})
// kilocode_change end
const flags = yield* RuntimeFlags.Service
const restricted = yield* SandboxPolicy.networkRestricted(input.session.id) // kilocode_change
const sandboxed = (yield* SandboxPolicy.status(input.session.id)).enabled // kilocode_change
const context = (args: Record<string, unknown>, options: ToolExecutionOptions): Tool.Context => {
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ const layer = Layer.effect(
tool.patch,
tool.plan,
...(["cli", "vscode"].includes(flags.client) ? [tool.suggest] : []),
...KiloToolRegistry.extra(kilo, cfg),
...KiloToolRegistry.extra(kilo, cfg, flags),
...(tool.execute ? [tool.execute] : []),
...(flags.experimentalLspTool ? [tool.lsp] : []),
],
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/test/kilocode/board-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Agent } from "../../src/agent/agent"
import { Session } from "../../src/session/session"
import { SessionStatus } from "../../src/session/status"
Expand All @@ -33,6 +34,7 @@ const it = testEffect(
BackgroundJob.node,
SessionProjector.node,
Config.node,
RuntimeFlags.node,
Database.node,
Agent.node,
Truncate.node,
Expand Down
65 changes: 65 additions & 0 deletions packages/opencode/test/kilocode/board-enabled.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect } from "bun:test"
import { ConfigProvider, Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { BoardEnabled } from "../../src/kilocode/board/enabled"
import { it } from "../lib/effect"

const fromEnv = (input: Record<string, unknown>) =>
AppNodeBuilder.build(RuntimeFlags.node).pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))

const resolve = (config: boolean | undefined, input: Record<string, unknown>) =>
Effect.gen(function* () {
const flags = yield* RuntimeFlags.Service
return BoardEnabled.resolve({ config, flag: flags.experimentalSharedAgentBoard })
}).pipe(Effect.provide(fromEnv(input)))

describe("shared agent board enablement", () => {
it.effect("enables when the config key is true", () =>
Effect.gen(function* () {
expect(yield* resolve(true, {})).toBe(true)
}),
)

it.effect("stays disabled when config is false and the env flag is unset", () =>
Effect.gen(function* () {
expect(yield* resolve(false, {})).toBe(false)
expect(yield* resolve(undefined, {})).toBe(false)
}),
)

it.effect("enables when the specific env flag is true", () =>
Effect.gen(function* () {
expect(yield* resolve(undefined, { KILO_EXPERIMENTAL_SHARED_AGENT_BOARD: "true" })).toBe(true)
}),
)

it.effect("stays disabled when the specific env flag is false", () =>
Effect.gen(function* () {
expect(yield* resolve(undefined, { KILO_EXPERIMENTAL_SHARED_AGENT_BOARD: "false" })).toBe(false)
}),
)

it.effect("enables when the KILO_EXPERIMENTAL umbrella is true", () =>
Effect.gen(function* () {
expect(yield* resolve(undefined, { KILO_EXPERIMENTAL: "true" })).toBe(true)
}),
)

it.effect("lets the specific flag override the umbrella", () =>
Effect.gen(function* () {
expect(
yield* resolve(undefined, {
KILO_EXPERIMENTAL: "true",
KILO_EXPERIMENTAL_SHARED_AGENT_BOARD: "false",
}),
).toBe(false)
}),
)

it.effect("keeps the env path enabled when config is explicitly false", () =>
Effect.gen(function* () {
expect(yield* resolve(false, { KILO_EXPERIMENTAL_SHARED_AGENT_BOARD: "true" })).toBe(true)
}),
)
})
2 changes: 2 additions & 0 deletions packages/opencode/test/kilocode/board-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { Permission } from "../../src/permission"
import { Tool } from "../../src/tool/tool"
import { ToolRegistry } from "../../src/tool/registry"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { BoardReadTool, BoardPostTool } from "../../src/kilocode/tool/board"
import { BoardStore } from "../../src/kilocode/board/store"
import { KiloTask } from "../../src/kilocode/tool/task"
Expand All @@ -34,6 +35,7 @@ const it = testEffect(
BackgroundJob.node,
Agent.node,
Config.node,
RuntimeFlags.node,
Database.node,
Truncate.node,
CrossSpawnSpawner.node,
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/kilocode/chart-tool-gating.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function ids(client: string) {
const prev = process.env.KILO_CLIENT
try {
process.env.KILO_CLIENT = client
return KiloToolRegistry.extra(tools, {}).map((t) => t.id)
return KiloToolRegistry.extra(tools, {}, { experimentalSharedAgentBoard: false }).map((t) => t.id)
} finally {
if (prev === undefined) delete process.env.KILO_CLIENT
else process.env.KILO_CLIENT = prev
Expand Down
Loading
Loading