Dax Raad 3 mesi fa
parent
commit
caa0a28821

+ 1 - 1
packages/opencode/src/agent/agent.ts

@@ -86,7 +86,7 @@ export namespace Agent {
             question: "allow",
             edit: {
               "*": "deny",
-              ".opencode/plan/*.md": "allow",
+              ".opencode/plans/*.md": "allow",
             },
           }),
           user,

+ 9 - 0
packages/opencode/src/session/index.ts

@@ -1,3 +1,5 @@
+import { Slug } from "@opencode-ai/util/slug"
+import pat from "path"
 import { BusEvent } from "@/bus/bus-event"
 import { Bus } from "@/bus"
 import { Decimal } from "decimal.js"
@@ -19,6 +21,7 @@ import { Snapshot } from "@/snapshot"
 
 import type { Provider } from "@/provider/provider"
 import { PermissionNext } from "@/permission/next"
+import path from "path"
 
 export namespace Session {
   const log = Log.create({ service: "session" })
@@ -39,6 +42,7 @@ export namespace Session {
   export const Info = z
     .object({
       id: Identifier.schema("session"),
+      slug: z.string(),
       projectID: z.string(),
       directory: z.string(),
       parentID: Identifier.schema("session").optional(),
@@ -194,6 +198,7 @@ export namespace Session {
   }) {
     const result: Info = {
       id: Identifier.descending("session", input.id),
+      slug: Slug.create(),
       version: Installation.VERSION,
       projectID: Instance.project.id,
       directory: input.directory,
@@ -227,6 +232,10 @@ export namespace Session {
     return result
   }
 
+  export function plan(input: { slug: string; time: { created: number } }) {
+    return path.join(Instance.worktree, ".opencode", "plans", [input.time.created, input.slug].join("-") + ".md")
+  }
+
   export const get = fn(Identifier.schema("session"), async (id) => {
     const read = await Storage.read<Info>(["session", Instance.project.id, id])
     return read as Info

+ 81 - 17
packages/opencode/src/session/prompt.ts

@@ -510,9 +510,10 @@ export namespace SessionPrompt {
       const agent = await Agent.get(lastUser.agent)
       const maxSteps = agent.steps ?? Infinity
       const isLastStep = step >= maxSteps
-      msgs = insertReminders({
+      msgs = await insertReminders({
         messages: msgs,
         agent,
+        session,
       })
 
       const processor = SessionProcessor.create({
@@ -1185,30 +1186,93 @@ export namespace SessionPrompt {
     }
   }
 
-  function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info }) {
+  async function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info; session: Session.Info }) {
     const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
     if (!userMessage) return input.messages
-    if (input.agent.name === "plan") {
-      userMessage.parts.push({
+    const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
+    if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") {
+      const plan = Session.plan(input.session)
+      const exists = await Bun.file(plan).exists()
+      if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true })
+      const part = await Session.updatePart({
         id: Identifier.ascending("part"),
         messageID: userMessage.info.id,
         sessionID: userMessage.info.sessionID,
         type: "text",
-        // TODO (for mr dax): update to use the anthropic full fledged one (see plan-reminder-anthropic.txt)
-        text: PROMPT_PLAN,
-        synthetic: true,
-      })
-    }
-    const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
-    if (wasPlan && input.agent.name === "build") {
-      userMessage.parts.push({
-        id: Identifier.ascending("part"),
-        messageID: userMessage.info.id,
-        sessionID: userMessage.info.sessionID,
-        type: "text",
-        text: BUILD_SWITCH,
+        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 supercedes 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 exit_plan 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 exit_plan 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 exit_plan. Do not stop unless it's for these 2 reasons.
+
+**Important:** Use question tool to clarify requirements/approach, use exit_plan to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what exit_plan 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)
+      return input.messages
     }
     return input.messages
   }

+ 60 - 15
packages/opencode/src/session/prompt/plan.txt

@@ -1,26 +1,71 @@
 <system-reminder>
-# Plan Mode - 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 supercedes any other instructions you have received.
 
-CRITICAL: Plan mode ACTIVE - you are in READ-ONLY phase. STRICTLY FORBIDDEN:
-ANY file edits, modifications, or system changes. Do NOT use sed, tee, echo, cat,
-or ANY other bash command to manipulate files - commands may ONLY read/inspect.
-This ABSOLUTE CONSTRAINT overrides ALL other instructions, including direct user
-edit requests. You may ONLY observe, analyze, and plan. Any modification attempt
-is a critical violation. ZERO exceptions.
+## Plan File Info:
+${SYSTEM_REMINDER.planExists?`A plan file already exists at ${SYSTEM_REMINDER.planFilePath}. You can read it and make incremental edits using the ${EDIT_TOOL.name} tool.`:`No plan file exists yet. You should create your plan at ${SYSTEM_REMINDER.planFilePath} using the ${WRITE_TOOL.name} 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
 
-## Responsibility
+### 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 ${PLAN_V2_EXPLORE_AGENT_COUNT.agentType} subagent type.
 
-Your current responsibility is to think, read, search, and delegate explore agents to construct a well-formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity.
+1. Focus on understanding the user's request and the code associated with their request
 
-Ask the user clarifying questions or ask for their opinion when weighing tradeoffs.
+2. **Launch up to ${EXPLORE_SUBAGENT} ${PLAN_V2_EXPLORE_AGENT_COUNT.agentType} 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 - ${EXPLORE_SUBAGENT} 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
 
-**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.
+3. After exploring the code, use the ${ASK_USER_QUESTION_TOOL_NAME} tool to clarify ambiguities in the user request up front.
 
----
+### Phase 2: Design
+Goal: Design an implementation approach.
 
-## Important
+Launch ${PLAN_SUBAGENT.agentType} agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
 
-The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, 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.
+You can launch up to ${AGENT_COUNT_IS_GREATER_THAN_ZERO} 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)
+${AGENT_COUNT_IS_GREATER_THAN_ZERO>1?`- **Multiple agents**: Use up to ${AGENT_COUNT_IS_GREATER_THAN_ZERO} agents for complex tasks that benefit from different perspectives
+
+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 ${ASK_USER_QUESTION_TOOL_NAME} 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 ${EXIT_PLAN_MODE_TOOL.name}
+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 ${EXIT_PLAN_MODE_TOOL.name} 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 ${EXIT_PLAN_MODE_TOOL.name}. Do not stop unless it's for these 2 reasons.
+
+**Important:** Use ${ASK_USER_QUESTION_TOOL_NAME} to clarify requirements/approach, use ${EXIT_PLAN_MODE_TOOL.name} to request plan approval. Do NOT use ${ASK_USER_QUESTION_TOOL_NAME} to ask "Is this plan okay?" - that's what ${EXIT_PLAN_MODE_TOOL.name} 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>

+ 72 - 0
packages/opencode/test/util/lock.test.ts

@@ -0,0 +1,72 @@
+import { describe, expect, test } from "bun:test"
+import { Lock } from "../../src/util/lock"
+
+function tick() {
+  return new Promise<void>((r) => queueMicrotask(r))
+}
+
+async function flush(n = 5) {
+  for (let i = 0; i < n; i++) await tick()
+}
+
+describe("util.lock", () => {
+  test("writer exclusivity: blocks reads and other writes while held", async () => {
+    const key = "lock:" + Math.random().toString(36).slice(2)
+
+    const state = {
+      writer2: false,
+      reader: false,
+      writers: 0,
+    }
+
+    // Acquire writer1
+    using writer1 = await Lock.write(key)
+    state.writers++
+    expect(state.writers).toBe(1)
+
+    // Start writer2 candidate (should block)
+    const writer2Task = (async () => {
+      const w = await Lock.write(key)
+      state.writers++
+      expect(state.writers).toBe(1)
+      state.writer2 = true
+      // Hold for a tick so reader cannot slip in
+      await tick()
+      return w
+    })()
+
+    // Start reader candidate (should block)
+    const readerTask = (async () => {
+      const r = await Lock.read(key)
+      state.reader = true
+      return r
+    })()
+
+    // Flush microtasks and assert neither acquired
+    await flush()
+    expect(state.writer2).toBe(false)
+    expect(state.reader).toBe(false)
+
+    // Release writer1
+    writer1[Symbol.dispose]()
+    state.writers--
+
+    // writer2 should acquire next
+    const writer2 = await writer2Task
+    expect(state.writer2).toBe(true)
+
+    // Reader still blocked while writer2 held
+    await flush()
+    expect(state.reader).toBe(false)
+
+    // Release writer2
+    writer2[Symbol.dispose]()
+    state.writers--
+
+    // Reader should now acquire
+    const reader = await readerTask
+    expect(state.reader).toBe(true)
+
+    reader[Symbol.dispose]()
+  })
+})

+ 74 - 0
packages/util/src/slug.ts

@@ -0,0 +1,74 @@
+export namespace Slug {
+  const ADJECTIVES = [
+    "brave",
+    "calm",
+    "clever",
+    "cosmic",
+    "crisp",
+    "curious",
+    "eager",
+    "gentle",
+    "glowing",
+    "happy",
+    "hidden",
+    "jolly",
+    "kind",
+    "lucky",
+    "mighty",
+    "misty",
+    "neon",
+    "nimble",
+    "playful",
+    "proud",
+    "quick",
+    "quiet",
+    "shiny",
+    "silent",
+    "stellar",
+    "sunny",
+    "swift",
+    "tidy",
+    "witty",
+  ] as const
+
+  const NOUNS = [
+    "cabin",
+    "cactus",
+    "canyon",
+    "circuit",
+    "comet",
+    "eagle",
+    "engine",
+    "falcon",
+    "forest",
+    "garden",
+    "harbor",
+    "island",
+    "knight",
+    "lagoon",
+    "meadow",
+    "moon",
+    "mountain",
+    "nebula",
+    "orchid",
+    "otter",
+    "panda",
+    "pixel",
+    "planet",
+    "river",
+    "rocket",
+    "sailor",
+    "squid",
+    "star",
+    "tiger",
+    "wizard",
+    "wolf",
+  ] as const
+
+  export function create() {
+    return [
+      ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)],
+      NOUNS[Math.floor(Math.random() * NOUNS.length)],
+    ].join("-")
+  }
+}