mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
feat(cli): align native plan with architect (#11170)
* feat(cli): align native plan with architect * fix(cli): support repo-root plan files * test(cli): focus native plan reminder assertions
This commit is contained in:
committed by
GitHub
parent
884c54f4f8
commit
38459184f2
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Make native Plan mode follow Architect-style planning behavior while preserving Plan mode restrictions and repo-root plan files.
|
||||
@@ -169,6 +169,7 @@ function planEditRules(worktree: string) {
|
||||
return {
|
||||
"*": "deny" as const,
|
||||
[path.join(".kilo", "plans", "*.md")]: "allow" as const,
|
||||
[path.join("plans", "*.md")]: "allow" as const,
|
||||
[path.join(".plans", "*.md")]: "allow" as const,
|
||||
[path.join(".opencode", "plans", "*.md")]: "allow" as const,
|
||||
[path.relative(worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow" as const,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<system-reminder>
|
||||
# Native Plan Mode
|
||||
|
||||
You are Kilo Code, an experienced technical leader who is inquisitive, skeptical, and an excellent planner.
|
||||
|
||||
Your job is to gather context, challenge assumptions, resolve design questions, and produce an implementation-ready plan that another agent can execute. You do not implement source-code changes.
|
||||
|
||||
## Planning Behavior
|
||||
|
||||
- Inspect the codebase and available local context before asking questions that can be answered without the user.
|
||||
- Interview the user about every important aspect of the plan until you reach shared understanding.
|
||||
- Walk down each branch of the design tree, resolving dependencies between decisions one by one.
|
||||
- Ask one question at a time, and include your recommended answer.
|
||||
- Continue until the important decisions are resolved or explicitly marked out of scope.
|
||||
- Challenge vague or overloaded terms such as "user", "account", "tenant", "job", "workflow", "session", or "state" until their meaning is precise in this codebase.
|
||||
- Cross-check user claims against the actual code and available context. If they conflict, call out the contradiction directly.
|
||||
- Use concrete scenarios and edge cases to test the proposed design.
|
||||
- Prefer short, actionable plans over long speculative documents.
|
||||
- Never provide level-of-effort estimates such as hours, days, or weeks.
|
||||
|
||||
## Plan Files
|
||||
|
||||
- You may create and edit plan Markdown files only.
|
||||
- Follow the latest Plan File reminder for the target plan location.
|
||||
- Prefer `.kilo/plans/` with a concise kebab-case filename based on the plan details when no exact plan path is provided.
|
||||
- Use repo-root `plans/`, `.plans/`, or `.opencode/plans/` only when requested or required by the repo/client and your permissions allow it.
|
||||
- Do not write the final plan or call `plan_exit` until the user chooses "Finalize and save the plan".
|
||||
- After final approval, write the final plan to the chosen plan file, then call `plan_exit` with the saved plan path.
|
||||
- Do not edit source files or non-plan documentation files.
|
||||
- Do not run mutating commands.
|
||||
- If implementation requires source edits or mutating commands, tell the user to switch to an implementation-capable agent.
|
||||
|
||||
## Completion Behavior
|
||||
|
||||
- Keep planning until the important design decisions are resolved or explicitly marked out of scope.
|
||||
- If material uncertainty remains, keep the plan open: summarize the current state, identify the most important unresolved decision, and ask exactly one next question with your recommended answer.
|
||||
- If the plan is implementation-ready but not saved, do not print the full plan in chat. Give a concise draft-ready summary, then ask exactly one question with these choices:
|
||||
1. Finalize and save the plan
|
||||
2. Continue refining
|
||||
- Recommend "Finalize and save the plan" only when the goal, constraints, affected boundaries, data flow, failure modes, rollout or migration path, and validation plan are addressed or explicitly out of scope.
|
||||
- If the user chooses "Finalize and save the plan", write the complete finalized Markdown plan to the chosen plan file, then call `plan_exit` as described above.
|
||||
- If the user chooses "Continue refining", keep planning and do not write the final plan or call `plan_exit`.
|
||||
- After `plan_exit`, rely on the client follow-up to ask whether the user wants to implement the saved plan in a new session.
|
||||
- Do not implement source or documentation changes as this agent.
|
||||
|
||||
Saved plans should be concise and actionable. Prefer a clear ordered task list over a lengthy design document. Include only the context, decisions, risks, validation steps, and open questions another implementation-capable agent needs to execute safely.
|
||||
</system-reminder>
|
||||
@@ -20,7 +20,7 @@ import { environmentDetails, type EditorContext } from "@/kilocode/editor-contex
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import PROMPT_PLAN from "@/session/prompt/plan.txt"
|
||||
import NATIVE_PLAN_PROMPT from "@/kilocode/session/native-plan-prompt.txt"
|
||||
import CODE_SWITCH from "@/session/prompt/code-switch.txt"
|
||||
|
||||
export namespace KiloSessionPrompt {
|
||||
@@ -282,21 +282,7 @@ export namespace KiloSessionPrompt {
|
||||
const ctx = Instance.bind(() => Instance.current)()
|
||||
const plan = Session.plan(input.session, ctx)
|
||||
|
||||
if (mode(input.agent.name) === "plan") {
|
||||
add(
|
||||
[
|
||||
PROMPT_PLAN,
|
||||
"",
|
||||
"## Plan File",
|
||||
"Use the plan path specified by the user or project instructions when present and permissions allow it.",
|
||||
"If none is specified, create a plan in .kilo/plans/ using a concise kebab-case filename based on the plan details.",
|
||||
"Do not choose .kilo/plans/ when instructions specify an allowed plan path such as .plans/.",
|
||||
"You may write/edit plan Markdown files only. Do not edit source files.",
|
||||
"When finalizing, call plan_exit with the path of the plan file you wrote.",
|
||||
].join("\n"),
|
||||
)
|
||||
return
|
||||
}
|
||||
if (mode(input.agent.name) === "plan") add(NATIVE_PLAN_PROMPT)
|
||||
|
||||
const file = input.messages ? PlanFile.latest(input.messages) : undefined
|
||||
const saved = PlanFile.resolve(file, ctx)
|
||||
@@ -311,7 +297,7 @@ export namespace KiloSessionPrompt {
|
||||
"## Plan File",
|
||||
info,
|
||||
"Use the chosen plan path as the main plan file. Do not write or edit other files unless the user explicitly asks and your permissions allow it.",
|
||||
"Project/user instructions about plan location (for example .plans/) are authorized when permissions allow them; they do not conflict with this reminder. When finalizing, call plan_exit with the path of the plan file you wrote.",
|
||||
"Project/user instructions about plan location (for example plans/ or .plans/) are authorized when permissions allow them; they do not conflict with this reminder. When finalizing, call plan_exit with the path of the plan file you wrote.",
|
||||
'Before creating or updating the plan file, or calling plan_exit, ask the user to choose exactly one of: "Finalize and save the plan" or "Continue refining". If the user chooses to finalize, write the main plan file, then call plan_exit.',
|
||||
].join("\n")
|
||||
add(`<system-reminder>\n${body}\n</system-reminder>`)
|
||||
|
||||
@@ -411,17 +411,19 @@ export const layer = Layer.effect(
|
||||
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
|
||||
if (!userMessage) return input.messages
|
||||
|
||||
// kilocode_change start - shared planning reminder path
|
||||
// No-op unless the active agent is plan-like.
|
||||
yield* Effect.promise(() =>
|
||||
KiloSessionPrompt.insertPlanReminders({
|
||||
agent: input.agent,
|
||||
session: input.session,
|
||||
userMessage,
|
||||
messages: input.messages,
|
||||
}),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
if (!flags.experimentalPlanMode) {
|
||||
// kilocode_change start - inject plan file path so agent writes to .kilo/plans/
|
||||
yield* Effect.promise(() =>
|
||||
KiloSessionPrompt.insertPlanReminders({
|
||||
agent: input.agent,
|
||||
session: input.session,
|
||||
userMessage,
|
||||
messages: input.messages,
|
||||
}),
|
||||
)
|
||||
// kilocode_change end
|
||||
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
|
||||
if (wasPlan && input.agent.name === "code") {
|
||||
// kilocode_change - renamed from "build" to "code"
|
||||
@@ -454,91 +456,9 @@ export const layer = Layer.effect(
|
||||
return input.messages
|
||||
}
|
||||
|
||||
if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages
|
||||
|
||||
const ctx = yield* InstanceState.context
|
||||
const plan = Session.plan(input.session, ctx)
|
||||
const exists = yield* fsys.existsSafe(plan)
|
||||
if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die))
|
||||
const part = yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: userMessage.info.id,
|
||||
sessionID: userMessage.info.sessionID,
|
||||
type: "text",
|
||||
text: `<system-reminder>
|
||||
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
|
||||
|
||||
## Plan File Info:
|
||||
${exists ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`}
|
||||
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
||||
|
||||
## Plan Workflow
|
||||
|
||||
### Phase 1: Initial Understanding
|
||||
Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type.
|
||||
|
||||
1. Focus on understanding the user's request and the code associated with their request
|
||||
|
||||
2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
|
||||
- Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
|
||||
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
|
||||
- Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
|
||||
- If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
|
||||
|
||||
3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
|
||||
|
||||
### Phase 2: Design
|
||||
Goal: Design an implementation approach.
|
||||
|
||||
Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
|
||||
|
||||
You can launch up to 1 agent(s) in parallel.
|
||||
|
||||
**Guidelines:**
|
||||
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
|
||||
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
|
||||
|
||||
Examples of when to use multiple agents:
|
||||
- The task touches multiple parts of the codebase
|
||||
- It's a large refactor or architectural change
|
||||
- There are many edge cases to consider
|
||||
- You'd benefit from exploring different approaches
|
||||
|
||||
Example perspectives by task type:
|
||||
- New feature: simplicity vs performance vs maintainability
|
||||
- Bug fix: root cause vs workaround vs prevention
|
||||
- Refactoring: minimal change vs clean architecture
|
||||
|
||||
In the agent prompt:
|
||||
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
|
||||
- Describe requirements and constraints
|
||||
- Request a detailed implementation plan
|
||||
|
||||
### Phase 3: Review
|
||||
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
|
||||
1. Read the critical files identified by agents to deepen your understanding
|
||||
2. Ensure that the plans align with the user's original request
|
||||
3. Use question tool to clarify any remaining questions with the user
|
||||
|
||||
### Phase 4: Final Plan
|
||||
Goal: Write your final plan to the plan file (the only file you can edit).
|
||||
- Include only your recommended approach, not all alternatives
|
||||
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
|
||||
- Include the paths of critical files to be modified
|
||||
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
|
||||
|
||||
### Phase 5: Call plan_exit tool
|
||||
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning.
|
||||
This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons.
|
||||
|
||||
**Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does.
|
||||
|
||||
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
|
||||
</system-reminder>`,
|
||||
synthetic: true,
|
||||
})
|
||||
userMessage.parts.push(part)
|
||||
// kilocode_change start - replace native Plan's separate prompt with the shared reminder above
|
||||
return input.messages
|
||||
// kilocode_change end
|
||||
})
|
||||
|
||||
const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: {
|
||||
|
||||
@@ -70,6 +70,7 @@ test("plan agent still hard-denies non-plan edits after user edit allow", async
|
||||
expect(plan).toBeDefined()
|
||||
expect(Permission.evaluate("edit", "src/output.log", plan!.permission).action).toBe("deny")
|
||||
expect(Permission.evaluate("edit", ".kilo/plans/fix.md", plan!.permission).action).toBe("allow")
|
||||
expect(Permission.evaluate("edit", "plans/fix.md", plan!.permission).action).toBe("allow")
|
||||
expect(Permission.evaluate("edit", ".plans/fix.md", plan!.permission).action).toBe("allow")
|
||||
},
|
||||
})
|
||||
|
||||
@@ -149,6 +149,13 @@ async function waitQuestion(sessionID: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function content(message: MessageV2.WithParts) {
|
||||
return message.parts
|
||||
.filter((part): part is MessageV2.TextPart => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
describe("plan_exit detection", () => {
|
||||
test("PlanFollowup.ask triggers when plan_exit tool is present", () =>
|
||||
withInstance(async () => {
|
||||
@@ -641,7 +648,7 @@ describe("plan_exit detection", () => {
|
||||
expect(text).not.toContain("No plan file exists yet")
|
||||
}))
|
||||
|
||||
test("plan reminder prefers project plan path instructions over fallback", () =>
|
||||
test("native plan reminder prefers project plan path instructions over fallback", () =>
|
||||
withInstance(async () => {
|
||||
const session = await sessions.create({})
|
||||
const id = MessageID.ascending()
|
||||
@@ -660,7 +667,7 @@ describe("plan_exit detection", () => {
|
||||
messageID: id,
|
||||
sessionID: session.id,
|
||||
type: "text",
|
||||
text: "Create a plan. AGENTS says plans go in .plans/.",
|
||||
text: "Create a plan. AGENTS says plans go in plans/.",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -675,12 +682,62 @@ describe("plan_exit detection", () => {
|
||||
const part = user.parts.at(-1)
|
||||
const text = part?.type === "text" ? part.text : ""
|
||||
expect(text).toContain("Use the plan path specified by the user or project instructions")
|
||||
expect(text).toContain("Do not choose .kilo/plans/")
|
||||
expect(text).toContain(".plans/")
|
||||
expect(text).toContain("plans/ or .plans/")
|
||||
expect(text).toContain("If none is specified")
|
||||
expect(text).not.toContain(Session.plan(session, Instance.current))
|
||||
}))
|
||||
|
||||
test("native plan reminder reuses custom plan_exit path when refining", () =>
|
||||
withInstance(async () => {
|
||||
const seeded = await seed({
|
||||
tools: [
|
||||
{
|
||||
tool: "plan_exit",
|
||||
input: { path: ".plans/fix.md" },
|
||||
output: "Plan is ready at .plans/fix.md. Ending planning turn.",
|
||||
},
|
||||
],
|
||||
})
|
||||
const file = path.join(Instance.worktree, ".plans", "fix.md")
|
||||
await fs.mkdir(path.dirname(file), { recursive: true })
|
||||
await Bun.write(file, "Do implementation step 1")
|
||||
|
||||
const session = await sessions.get(seeded.sessionID)
|
||||
const id = MessageID.ascending()
|
||||
const user: MessageV2.WithParts = {
|
||||
info: {
|
||||
id,
|
||||
role: "user",
|
||||
sessionID: seeded.sessionID,
|
||||
time: { created: Date.now() },
|
||||
agent: "plan",
|
||||
model,
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: PartID.ascending(),
|
||||
messageID: id,
|
||||
sessionID: seeded.sessionID,
|
||||
type: "text",
|
||||
text: "Continue refining",
|
||||
},
|
||||
],
|
||||
}
|
||||
await KiloSessionPrompt.insertPlanReminders({
|
||||
agent: { name: "plan", options: {} },
|
||||
session,
|
||||
userMessage: user,
|
||||
messages: [...seeded.messages, user],
|
||||
})
|
||||
|
||||
const text = content(user)
|
||||
expect(text).toContain("The current saved plan file is")
|
||||
expect(text.replaceAll(path.sep, "/")).toContain(".plans/fix.md")
|
||||
expect(text).toContain("Read and edit this file when refining the plan")
|
||||
expect(text).toContain("experienced technical leader")
|
||||
expect(text).not.toContain("No plan file exists yet")
|
||||
}))
|
||||
|
||||
test("architect reminder prefers project plan path instructions over fallback", () =>
|
||||
withInstance(async () => {
|
||||
const session = await sessions.create({})
|
||||
@@ -700,7 +757,7 @@ describe("plan_exit detection", () => {
|
||||
messageID: id,
|
||||
sessionID: session.id,
|
||||
type: "text",
|
||||
text: "Create a plan. AGENTS says plans go in .plans/.",
|
||||
text: "Create a plan. AGENTS says plans go in plans/.",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -716,7 +773,7 @@ describe("plan_exit detection", () => {
|
||||
const text = part?.type === "text" ? part.text : ""
|
||||
expect(text).toContain("Use the plan path specified by the user or project instructions")
|
||||
expect(text).toContain("If none is specified")
|
||||
expect(text).toContain(".plans/")
|
||||
expect(text).toContain("plans/ or .plans/")
|
||||
expect(text).not.toContain("Default to")
|
||||
expect(text).not.toContain("A fallback plan file exists")
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user