From 18c03848a5f7d6231626b76b1ed86525faa82d51 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 18 Sep 2026 11:23:38 +0200 Subject: [PATCH 1/2] fix(cli): remove vestigial KILO_EXPERIMENTAL_PLAN_MODE flag Plan mode already runs unconditionally, so the flag only gated the Plan to Code handoff reminder. Keep the upstream default behavior: always append the saved plan file hint when switching from a planning agent to Code. --- .../remove-experimental-plan-mode-flag.md | 5 ++ packages/core/src/flag/flag.ts | 2 - .../docs/cli-side/plan-mode-over-prompting.md | 2 +- packages/opencode/src/effect/runtime-flags.ts | 1 - packages/opencode/src/session/reminders.ts | 24 ------- .../test/effect/runtime-flags.test.ts | 1 - .../test/kilocode/reminders-separator.test.ts | 69 ++++++++++++++----- 7 files changed, 58 insertions(+), 46 deletions(-) create mode 100644 .changeset/remove-experimental-plan-mode-flag.md diff --git a/.changeset/remove-experimental-plan-mode-flag.md b/.changeset/remove-experimental-plan-mode-flag.md new file mode 100644 index 000000000000..254e3a443baa --- /dev/null +++ b/.changeset/remove-experimental-plan-mode-flag.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Always include the saved plan file hint when switching from Plan to Code, and remove the vestigial `KILO_EXPERIMENTAL_PLAN_MODE` flag. diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index e4e1f927bcca..c22a64d54eba 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -96,8 +96,6 @@ export const Flag = { KILO_EXPERIMENTAL_LSP_TOOL: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_LSP_TOOL"), // kilocode_change - KILO_EXPERIMENTAL_PLAN_MODE: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_PLAN_MODE"), // kilocode_change - KILO_EXPERIMENTAL_SCOUT: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SCOUT"), // kilocode_change KILO_EXPERIMENTAL_MARKDOWN: !falsy("KILO_EXPERIMENTAL_MARKDOWN"), // kilocode_change diff --git a/packages/kilo-vscode/docs/cli-side/plan-mode-over-prompting.md b/packages/kilo-vscode/docs/cli-side/plan-mode-over-prompting.md index 3a9887c58bce..3ab938b51ee9 100644 --- a/packages/kilo-vscode/docs/cli-side/plan-mode-over-prompting.md +++ b/packages/kilo-vscode/docs/cli-side/plan-mode-over-prompting.md @@ -3,7 +3,7 @@ **Priority:** P1 **Issue:** [#6143](https://github.com/Kilo-Org/kilocode/issues/6143) -An experimental plan mode prompt exists (`Flag.KILO_EXPERIMENTAL_PLAN_MODE`) that is verbose. Agent still tends to ask "Should I implement this?" repeatedly. +The plan mode prompt is verbose. The agent still tends to ask "Should I implement this?" repeatedly. ## Remaining Work diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index c706740749aa..e2ce52f23ff4 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -52,7 +52,6 @@ export class Service extends ConfigService.Service()("@opencode/Runtime experimentalLspTy: bool("KILO_EXPERIMENTAL_LSP_TY"), experimentalLspTool: enabledByExperimental("KILO_EXPERIMENTAL_LSP_TOOL"), experimentalOxfmt: enabledByExperimental("KILO_EXPERIMENTAL_OXFMT"), - experimentalPlanMode: enabledByExperimental("KILO_EXPERIMENTAL_PLAN_MODE"), experimentalCodeMode: enabledByExperimental("KILO_EXPERIMENTAL_CODE_MODE"), experimentalEventSystem: enabledByExperimental("KILO_EXPERIMENTAL_EVENT_SYSTEM"), experimentalSessionSwitcher: enabledByExperimental("KILO_EXPERIMENTAL_SESSION_SWITCHER"), // kilocode_change diff --git a/packages/opencode/src/session/reminders.ts b/packages/opencode/src/session/reminders.ts index 7526f19e049d..5912bdb575e6 100644 --- a/packages/opencode/src/session/reminders.ts +++ b/packages/opencode/src/session/reminders.ts @@ -4,20 +4,16 @@ import { KiloSessionPrompt } from "@/kilocode/session/prompt" // kilocode_change import { Agent } from "@/agent/agent" import { FSUtil } from "@opencode-ai/core/fs-util" import { InstanceState } from "@/effect/instance-state" -import { RuntimeFlags } from "@/effect/runtime-flags" import { PartID } from "./schema" -import { MessageV2 } from "./message-v2" import { Session } from "./session" import { SessionV1 } from "@opencode-ai/core/v1/session" import CODE_SWITCH from "./prompt/code-switch.txt" // kilocode_change -import ASK_CODE_SWITCH from "@/kilocode/session/ask-code-switch.txt" // kilocode_change export const apply = Effect.fn("SessionReminders.apply")(function* (input: { messages: SessionV1.WithParts[] agent: Agent.Info session: Session.Info }) { - const flags = yield* RuntimeFlags.Service const fsys = yield* FSUtil.Service const sessions = yield* Session.Service const userMessage = input.messages.findLast((msg) => msg.info.role === "user") @@ -40,26 +36,6 @@ export const apply = Effect.fn("SessionReminders.apply")(function* (input: { if (switched) userMessage.parts.push(yield* sessions.updatePart(switched)) // kilocode_change end - if (!flags.experimentalPlanMode) { - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") - if ( - wasPlan && - input.agent.name === "code" && - !userMessage.parts.some((part) => part.type === "text" && part.text === ASK_CODE_SWITCH) - ) { - // kilocode_change - renamed from "build" to "code" - userMessage.parts.push({ - id: PartID.ascending(), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: `\n\n${CODE_SWITCH}`, // kilocode_change - separated from user content - synthetic: true, - }) - } - return input.messages - } - const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { const ctx = yield* InstanceState.context diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index 27e361268315..73550ddad0f4 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -85,7 +85,6 @@ describe("RuntimeFlags", () => { expect(flags.experimentalLspTy).toBe(false) expect(flags.experimentalLspTool).toBe(true) expect(flags.experimentalOxfmt).toBe(true) - expect(flags.experimentalPlanMode).toBe(true) expect(flags.experimentalEventSystem).toBe(true) expect(flags.experimentalWorkspaces).toBe(true) expect(flags.experimentalIconDiscovery).toBe(true) diff --git a/packages/opencode/test/kilocode/reminders-separator.test.ts b/packages/opencode/test/kilocode/reminders-separator.test.ts index a06c5d6bcb4f..09e1256b20db 100644 --- a/packages/opencode/test/kilocode/reminders-separator.test.ts +++ b/packages/opencode/test/kilocode/reminders-separator.test.ts @@ -1,14 +1,18 @@ import { describe, expect, test } from "bun:test" import { Effect, Layer } from "effect" +import fs from "fs/promises" +import path from "path" import { FSUtil } from "@opencode-ai/core/fs-util" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { Agent } from "../../src/agent/agent" -import { RuntimeFlags } from "../../src/effect/runtime-flags" +import { InstanceRef } from "../../src/effect/instance-ref" +import type { InstanceContext } from "../../src/project/instance-context" import { Session } from "../../src/session/session" import { SessionReminders } from "../../src/session/reminders" import { MessageV2 } from "../../src/session/message-v2" import { SessionID, MessageID, PartID } from "../../src/session/schema" +import { provideTestInstance, tmpdir } from "../fixture/fixture" const sessionID = SessionID.make("ses_reminders") const model = { providerID: ProviderV2.ID.make("openai"), modelID: ModelV2.ID.make("gpt-4") } @@ -57,36 +61,67 @@ function assistantPlan(): MessageV2.WithParts { } as unknown as MessageV2.WithParts } -const apply = (messages: MessageV2.WithParts[]) => - Effect.runPromise( +const session = { slug: "ses_reminders", time: { created: 0 } } as unknown as Session.Info + +function apply(ctx: InstanceContext, messages: MessageV2.WithParts[]) { + return Effect.runPromise( SessionReminders.apply({ messages, agent: { name: "code" } as unknown as Agent.Info, - session: {} as unknown as Session.Info, + session, }).pipe( + Effect.provideService(InstanceRef, ctx), Effect.provide( Layer.mergeAll( - RuntimeFlags.layer({ experimentalPlanMode: false }), - Layer.mock(Session.Service, {}), + Layer.mock(Session.Service, { + updatePart: (part: T) => Effect.succeed(part), + }), FSUtil.defaultLayer, ), ), ), ) +} describe("SessionReminders plan-to-code switch", () => { test("separates the code switch reminder from user text with blank lines", async () => { - const user = userMsg("write this to a file:") - const messages = [assistantPlan(), user] - const result = await apply(messages) + await using tmp = await tmpdir({ git: true }) + await provideTestInstance({ + directory: tmp.path, + fn: async (ctx) => { + const user = userMsg("write this to a file:") + const messages = [assistantPlan(), user] + const result = await apply(ctx, messages) + + expect(result).toBe(messages) + expect(user.parts).toHaveLength(2) + const userText = user.parts[0] as MessageV2.TextPart + const reminder = user.parts[1] as MessageV2.TextPart + expect(userText.text).toBe("write this to a file:") + expect(reminder.type).toBe("text") + expect(reminder.synthetic).toBe(true) + expect(reminder.text.startsWith("\n\n")).toBe(true) + }, + }) + }) + + test("appends the plan file hint without any experimental flag", async () => { + await using tmp = await tmpdir({ git: true }) + await provideTestInstance({ + directory: tmp.path, + fn: async (ctx) => { + const file = Session.plan(session, ctx) + await fs.mkdir(path.dirname(file), { recursive: true }) + await Bun.write(file, "step 1") - expect(result).toBe(messages) - expect(user.parts).toHaveLength(2) - const userText = user.parts[0] as MessageV2.TextPart - const reminder = user.parts[1] as MessageV2.TextPart - expect(userText.text).toBe("write this to a file:") - expect(reminder.type).toBe("text") - expect(reminder.synthetic).toBe(true) - expect(reminder.text.startsWith("\n\n")).toBe(true) + const user = userMsg("implement the plan:") + const messages = [assistantPlan(), user] + const result = await apply(ctx, messages) + + expect(result).toBe(messages) + const reminder = user.parts.at(-1) as MessageV2.TextPart + expect(reminder.text).toContain(`A plan file exists at ${file}`) + }, + }) }) }) From f5366f4e54bd0287f5c432c97ee04311bb6e65e9 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 18 Sep 2026 11:42:07 +0200 Subject: [PATCH 2/2] fix(cli): scope plan-to-code reminder to the code agent Append the CODE_SWITCH reminder only when entering the code agent from a planning agent (plan or architect). The broad non-plan check fired for read-only and other agents once the flag gate was removed. --- packages/opencode/src/session/reminders.ts | 3 +- .../test/kilocode/reminders-separator.test.ts | 42 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/session/reminders.ts b/packages/opencode/src/session/reminders.ts index 5912bdb575e6..392305c2ec0d 100644 --- a/packages/opencode/src/session/reminders.ts +++ b/packages/opencode/src/session/reminders.ts @@ -37,7 +37,8 @@ export const apply = Effect.fn("SessionReminders.apply")(function* (input: { // kilocode_change end const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") - if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { + const wasPlanning = assistantMessage?.info.agent === "plan" || assistantMessage?.info.agent === "architect" + if (input.agent.name === "code" && wasPlanning) { const ctx = yield* InstanceState.context const plan = Session.plan(input.session, ctx) const exists = yield* fsys.existsSafe(plan) diff --git a/packages/opencode/test/kilocode/reminders-separator.test.ts b/packages/opencode/test/kilocode/reminders-separator.test.ts index 09e1256b20db..7382a88a0b6b 100644 --- a/packages/opencode/test/kilocode/reminders-separator.test.ts +++ b/packages/opencode/test/kilocode/reminders-separator.test.ts @@ -40,7 +40,7 @@ function userMsg(text: string): MessageV2.WithParts { } } -function assistantPlan(): MessageV2.WithParts { +function assistant(agent = "plan"): MessageV2.WithParts { const id = MessageID.ascending() return { info: { @@ -48,7 +48,7 @@ function assistantPlan(): MessageV2.WithParts { role: "assistant", sessionID, time: { created: 0 }, - agent: "plan", + agent, modelID: model.modelID, providerID: model.providerID, parentID: "", @@ -63,11 +63,11 @@ function assistantPlan(): MessageV2.WithParts { const session = { slug: "ses_reminders", time: { created: 0 } } as unknown as Session.Info -function apply(ctx: InstanceContext, messages: MessageV2.WithParts[]) { +function apply(ctx: InstanceContext, messages: MessageV2.WithParts[], name = "code") { return Effect.runPromise( SessionReminders.apply({ messages, - agent: { name: "code" } as unknown as Agent.Info, + agent: { name } as unknown as Agent.Info, session, }).pipe( Effect.provideService(InstanceRef, ctx), @@ -90,7 +90,7 @@ describe("SessionReminders plan-to-code switch", () => { directory: tmp.path, fn: async (ctx) => { const user = userMsg("write this to a file:") - const messages = [assistantPlan(), user] + const messages = [assistant(), user] const result = await apply(ctx, messages) expect(result).toBe(messages) @@ -115,7 +115,7 @@ describe("SessionReminders plan-to-code switch", () => { await Bun.write(file, "step 1") const user = userMsg("implement the plan:") - const messages = [assistantPlan(), user] + const messages = [assistant(), user] const result = await apply(ctx, messages) expect(result).toBe(messages) @@ -124,4 +124,34 @@ describe("SessionReminders plan-to-code switch", () => { }, }) }) + + test("does not append the code switch reminder when entering a non-code agent", async () => { + await using tmp = await tmpdir({ git: true }) + await provideTestInstance({ + directory: tmp.path, + fn: async (ctx) => { + const user = userMsg("plan something:") + const result = await apply(ctx, [assistant(), user], "ask") + + expect(result).toBeDefined() + expect(user.parts).toHaveLength(1) + }, + }) + }) + + test("appends the reminder when switching from architect to code", async () => { + await using tmp = await tmpdir({ git: true }) + await provideTestInstance({ + directory: tmp.path, + fn: async (ctx) => { + const user = userMsg("implement the plan:") + const result = await apply(ctx, [assistant("architect"), user]) + + expect(result).toBeDefined() + const reminder = user.parts.at(-1) as MessageV2.TextPart + expect(reminder.synthetic).toBe(true) + expect(reminder.text.startsWith("\n\n")).toBe(true) + }, + }) + }) })