From 15fb582419da3b9a3dceb079915353ec09cbeb63 Mon Sep 17 00:00:00 2001 From: Daniel Saldarriaga Date: Thu, 20 Aug 2026 17:00:12 +0200 Subject: [PATCH] fix: make goal creation idempotent --- dist/server.js | 59 ++++++++++++++++++------- src/server.ts | 72 ++++++++++++++++++++++++------- test/server-v2.test.ts | 29 +++++++++++++ test/server.test.ts | 98 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 30 deletions(-) diff --git a/dist/server.js b/dist/server.js index 26d8606..cdf2e55 100644 --- a/dist/server.js +++ b/dist/server.js @@ -1144,6 +1144,9 @@ var NON_PROGRESS_TOOLS = new Set(["get_goal", "get_goal_history"]); var TASK_TERMINAL_STATES = new Set(["completed", "error", "cancelled"]); var PLAN_MODE_CREATE_NOTICE = 'Goal recorded while the session is in Plan mode, so execution is paused. Do not start implementation work now. Ask the user to switch to Build mode and resume the goal (for example with "/goal resume") to begin execution.'; var LIMITED_GOAL_NOTICE = "Safety limit reached. Do not start or continue substantive work for this goal. Summarize useful progress, remaining work, and blockers, then wait for the user to resume or edit the goal."; +var DUPLICATE_GOAL_NOTICE = "This non-closed goal already exists. Do not call create_goal or set_goal again. The existing objective and limits were preserved; repeated-call arguments were not applied. Use the returned goal state and continue only when its status permits execution."; +var CONFLICTING_GOAL_NOTICE = "A different non-closed goal already exists. Do not call create_goal or set_goal again. Report the conflict instead of replacing the goal; edit, clear, complete, or mark it unmet only when explicitly requested."; +var RESTRICTED_GOAL_NOTICE = "Goal execution is not allowed from the current restricted agent or while the goal is paused for Plan mode. Switch to Build mode and resume the goal before doing substantive work."; var activeContinuations = new Set; function restrictedAgentSet(options) { if (options?.allow_goal_execution_from_plan === true) @@ -1170,9 +1173,9 @@ Use the goal tools to handle this command: - If the arguments start with "edit ", update the current goal objective by calling update_goal_objective with the remaining text. - If the arguments start with "complete " or "done ", perform a completion audit against real artifacts and command output. Call update_goal with status "complete" only if the goal is achieved, using concise evidence from the audit. - If the arguments start with "unmet ", "blocked ", or "blocker ", call update_goal with status "unmet" only when the goal cannot be achieved or needs external input, using the remaining arguments as the blocker. -- Otherwise, create a new goal with create_goal. Use the full arguments as the objective. If the user includes explicit budget instructions, pass token_budget, max_auto_turns, or max_duration_seconds to create_goal rather than leaving those words in the objective. +- Otherwise, call get_goal first. If it returns a non-closed goal with the same objective, do not create it again; continue working from the returned state. If it returns a different non-closed goal, report that conflict instead of replacing it. Only when there is no non-closed goal, call create_goal once. Use the full arguments as the objective. If the user includes explicit budget instructions, pass token_budget, max_auto_turns, or max_duration_seconds to create_goal rather than leaving those words in the objective. -Create a goal only from these explicit command arguments. Do not infer a goal from unrelated session context. After create_goal succeeds, continue working toward the new goal.`; +Create a goal only from these explicit command arguments. Do not infer a goal from unrelated session context. After create_goal succeeds or returns an existing matching goal, never call it again for this command; continue working from the returned goal state.`; } function commandNameFromOptions(options) { const name = options?.command_name?.trim() || DEFAULT_COMMAND_NAME; @@ -1773,18 +1776,44 @@ function getGoalToolResult(goal) { } async function createGoalFromTool(input, context, services) { const planningOnly = services.isPlanAgent(context.agent); - const goal = await createGoal(context.sessionID, input.objective, { - tokenBudget: input.token_budget ?? services.options.default_token_budget ?? null, - maxAutoTurns: input.max_auto_turns ?? null, - maxDurationSeconds: input.max_duration_seconds ?? services.options.max_goal_duration_seconds ?? null, - noProgressTokenThreshold: services.options.no_progress_token_threshold ?? null, - maxNoProgressTurns: services.options.max_no_progress_turns ?? null, - agent: typeof context.agent === "string" ? context.agent : null, - initialStatus: planningOnly ? "paused" : "active" - }); + const objective = validateObjective(input.objective); + const existing = await getGoal(context.sessionID); + if (existing && !isClosedGoal(existing)) + return existingGoalResult(existing, objective, planningOnly); + let goal; + try { + goal = await createGoal(context.sessionID, input.objective, { + tokenBudget: input.token_budget ?? services.options.default_token_budget ?? null, + maxAutoTurns: input.max_auto_turns ?? null, + maxDurationSeconds: input.max_duration_seconds ?? services.options.max_goal_duration_seconds ?? null, + noProgressTokenThreshold: services.options.no_progress_token_threshold ?? null, + maxNoProgressTurns: services.options.max_no_progress_turns ?? null, + agent: typeof context.agent === "string" ? context.agent : null, + initialStatus: planningOnly ? "paused" : "active" + }); + } catch (error) { + if (!(error instanceof Error) || !error.message.includes("non-closed goal")) + throw error; + const raced = await getGoal(context.sessionID); + if (raced && !isClosedGoal(raced)) + return existingGoalResult(raced, objective, planningOnly); + throw error; + } await services.initializeUsage?.(context.sessionID); return JSON.stringify(planningOnly ? { goal, plan_mode_notice: PLAN_MODE_CREATE_NOTICE } : { goal }, null, 2); } +function isClosedGoal(goal) { + return goal.status === "complete" || goal.status === "unmet"; +} +function existingGoalResult(goal, requestedObjective, planningOnly) { + const reused = goal.objective === requestedObjective; + return JSON.stringify({ + goal, + ...reused ? { goal_reused: true, duplicate_goal_notice: DUPLICATE_GOAL_NOTICE } : { goal_conflict: true, goal_conflict_notice: CONFLICTING_GOAL_NOTICE }, + ...goal.status === "budgetLimited" || goal.status === "usageLimited" ? { goal_mode_notice: LIMITED_GOAL_NOTICE } : {}, + ...planningOnly || goal.stopReason === PLAN_MODE_STOP_REASON ? { plan_mode_notice: RESTRICTED_GOAL_NOTICE } : {} + }, null, 2); +} async function updateGoalObjectiveFromTool(input, context, services) { const requested = input.status ?? "active"; const planningOnly = requested === "active" && services.isPlanAgent(context.agent); @@ -2143,7 +2172,7 @@ var server = async ({ client }, options) => { } }, create_goal: { - description: "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + description: "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", args: { objective: z.string().min(1).max(4000).describe("The concrete objective to start pursuing."), token_budget: z.number().int().positive().nullable().optional().describe("Optional positive token budget."), @@ -2155,7 +2184,7 @@ var server = async ({ client }, options) => { } }, set_goal: { - description: "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + description: "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", args: { objective: z.string().min(1).max(4000).describe("The model-formulated concrete objective to start pursuing."), token_budget: z.number().int().positive().nullable().optional().describe("Optional positive token budget."), @@ -3019,7 +3048,7 @@ function goalToolsV2(services) { }, { name: "create_goal", - description: "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + description: "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", input: v2ObjectSchema({ objective: { type: "string", minLength: 1, maxLength: 4000, description: "The concrete objective to start pursuing." }, token_budget: { type: ["integer", "null"], minimum: 1, description: "Optional positive token budget." }, @@ -3033,7 +3062,7 @@ function goalToolsV2(services) { }, { name: "set_goal", - description: "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + description: "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", input: v2ObjectSchema({ objective: { type: "string", diff --git a/src/server.ts b/src/server.ts index 979f578..c0d6478 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,6 +15,7 @@ import { getGoalInternal, markGoalUnmet, pauseGoalForPlanMode, + PLAN_MODE_STOP_REASON, recordAssistantProgress, recordContinuationResult, recordPromptAgent, @@ -24,6 +25,7 @@ import { rollbackContinuationAttempt, setGoalStatus, updateGoalObjective, + validateObjective, } from "./state" import { compactionContext, continuationPrompt, limitPrompt, systemReminder } from "./prompts" @@ -82,6 +84,12 @@ const PLAN_MODE_CREATE_NOTICE = 'Goal recorded while the session is in Plan mode, so execution is paused. Do not start implementation work now. Ask the user to switch to Build mode and resume the goal (for example with "/goal resume") to begin execution.' const LIMITED_GOAL_NOTICE = "Safety limit reached. Do not start or continue substantive work for this goal. Summarize useful progress, remaining work, and blockers, then wait for the user to resume or edit the goal." +const DUPLICATE_GOAL_NOTICE = + "This non-closed goal already exists. Do not call create_goal or set_goal again. The existing objective and limits were preserved; repeated-call arguments were not applied. Use the returned goal state and continue only when its status permits execution." +const CONFLICTING_GOAL_NOTICE = + "A different non-closed goal already exists. Do not call create_goal or set_goal again. Report the conflict instead of replacing the goal; edit, clear, complete, or mark it unmet only when explicitly requested." +const RESTRICTED_GOAL_NOTICE = + "Goal execution is not allowed from the current restricted agent or while the goal is paused for Plan mode. Switch to Build mode and resume the goal before doing substantive work." const activeContinuations = new Set() type TaskState = "running" | "completed" | "error" | "cancelled" @@ -145,9 +153,9 @@ Use the goal tools to handle this command: - If the arguments start with "edit ", update the current goal objective by calling update_goal_objective with the remaining text. - If the arguments start with "complete " or "done ", perform a completion audit against real artifacts and command output. Call update_goal with status "complete" only if the goal is achieved, using concise evidence from the audit. - If the arguments start with "unmet ", "blocked ", or "blocker ", call update_goal with status "unmet" only when the goal cannot be achieved or needs external input, using the remaining arguments as the blocker. -- Otherwise, create a new goal with create_goal. Use the full arguments as the objective. If the user includes explicit budget instructions, pass token_budget, max_auto_turns, or max_duration_seconds to create_goal rather than leaving those words in the objective. +- Otherwise, call get_goal first. If it returns a non-closed goal with the same objective, do not create it again; continue working from the returned state. If it returns a different non-closed goal, report that conflict instead of replacing it. Only when there is no non-closed goal, call create_goal once. Use the full arguments as the objective. If the user includes explicit budget instructions, pass token_budget, max_auto_turns, or max_duration_seconds to create_goal rather than leaving those words in the objective. -Create a goal only from these explicit command arguments. Do not infer a goal from unrelated session context. After create_goal succeeds, continue working toward the new goal.` +Create a goal only from these explicit command arguments. Do not infer a goal from unrelated session context. After create_goal succeeds or returns an existing matching goal, never call it again for this command; continue working from the returned goal state.` } function commandNameFromOptions(options?: Options) { @@ -774,19 +782,53 @@ type GoalServices = { async function createGoalFromTool(input: CreateGoalArgs, context: ToolExecContext, services: GoalServices) { const planningOnly = services.isPlanAgent(context.agent) - const goal = await createGoal(context.sessionID, input.objective, { - tokenBudget: input.token_budget ?? services.options.default_token_budget ?? null, - maxAutoTurns: input.max_auto_turns ?? null, - maxDurationSeconds: input.max_duration_seconds ?? services.options.max_goal_duration_seconds ?? null, - noProgressTokenThreshold: services.options.no_progress_token_threshold ?? null, - maxNoProgressTurns: services.options.max_no_progress_turns ?? null, - agent: typeof context.agent === "string" ? context.agent : null, - initialStatus: planningOnly ? "paused" : "active", - }) + const objective = validateObjective(input.objective) + const existing = await getGoal(context.sessionID) + if (existing && !isClosedGoal(existing)) return existingGoalResult(existing, objective, planningOnly) + + let goal: GoalSnapshot + try { + goal = await createGoal(context.sessionID, input.objective, { + tokenBudget: input.token_budget ?? services.options.default_token_budget ?? null, + maxAutoTurns: input.max_auto_turns ?? null, + maxDurationSeconds: input.max_duration_seconds ?? services.options.max_goal_duration_seconds ?? null, + noProgressTokenThreshold: services.options.no_progress_token_threshold ?? null, + maxNoProgressTurns: services.options.max_no_progress_turns ?? null, + agent: typeof context.agent === "string" ? context.agent : null, + initialStatus: planningOnly ? "paused" : "active", + }) + } catch (error) { + if (!(error instanceof Error) || !error.message.includes("non-closed goal")) throw error + const raced = await getGoal(context.sessionID) + if (raced && !isClosedGoal(raced)) return existingGoalResult(raced, objective, planningOnly) + throw error + } await services.initializeUsage?.(context.sessionID) return JSON.stringify(planningOnly ? { goal, plan_mode_notice: PLAN_MODE_CREATE_NOTICE } : { goal }, null, 2) } +function isClosedGoal(goal: GoalSnapshot) { + return goal.status === "complete" || goal.status === "unmet" +} + +function existingGoalResult(goal: GoalSnapshot, requestedObjective: string, planningOnly: boolean) { + const reused = goal.objective === requestedObjective + return JSON.stringify( + { + goal, + ...(reused + ? { goal_reused: true, duplicate_goal_notice: DUPLICATE_GOAL_NOTICE } + : { goal_conflict: true, goal_conflict_notice: CONFLICTING_GOAL_NOTICE }), + ...(goal.status === "budgetLimited" || goal.status === "usageLimited" + ? { goal_mode_notice: LIMITED_GOAL_NOTICE } + : {}), + ...(planningOnly || goal.stopReason === PLAN_MODE_STOP_REASON ? { plan_mode_notice: RESTRICTED_GOAL_NOTICE } : {}), + }, + null, + 2, + ) +} + async function updateGoalObjectiveFromTool( input: { objective: string; status?: "active" | "paused" }, context: ToolExecContext, @@ -1225,7 +1267,7 @@ const server: Plugin = async ({ client }, options?: Options) => { }, create_goal: { description: - "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", args: { objective: z.string().min(1).max(4000).describe("The concrete objective to start pursuing."), token_budget: z.number().int().positive().nullable().optional().describe("Optional positive token budget."), @@ -1238,7 +1280,7 @@ const server: Plugin = async ({ client }, options?: Options) => { }, set_goal: { description: - "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", args: { objective: z.string().min(1).max(4000).describe("The model-formulated concrete objective to start pursuing."), token_budget: z.number().int().positive().nullable().optional().describe("Optional positive token budget."), @@ -2169,7 +2211,7 @@ function goalToolsV2(services: GoalServices): ToolV2Info[] { { name: "create_goal", description: - "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", input: v2ObjectSchema( { objective: { type: "string", minLength: 1, maxLength: 4000, description: "The concrete objective to start pursuing." }, @@ -2187,7 +2229,7 @@ function goalToolsV2(services: GoalServices): ToolV2Info[] { { name: "set_goal", description: - "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. Fails if a non-complete goal exists. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", + "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. If any non-closed goal exists, this returns the existing goal as either reused or conflicting and must not be retried. While the session is in Plan mode, the goal is recorded as paused and execution requires the user to switch to Build mode.", input: v2ObjectSchema( { objective: { diff --git a/test/server-v2.test.ts b/test/server-v2.test.ts index f8a81da..5a2a216 100644 --- a/test/server-v2.test.ts +++ b/test/server-v2.test.ts @@ -240,6 +240,33 @@ test("V2 create_goal recovers from a zero-filled state file", async () => { await cleanup() }) +test("V2 create_goal reuses the same active objective without reinitializing state", async () => { + const mock = makeMockContext({ auto_continue: false }) + const cleanup = await plugin.setup(mock as never) + await goalTool(mock, "create_goal").execute( + { objective: "finish V2 safely", token_budget: 100 }, + toolContext(), + ) + const before = await readFile(process.env.OPENCODE_GOAL_STATE_PATH!, "utf8") + + const duplicate = await goalTool(mock, "set_goal").execute( + { objective: " finish V2 safely ", token_budget: 999 }, + toolContext(), + ) + + expect(contentOf(duplicate)).toContain('"goal_reused": true') + expect(contentOf(duplicate)).toContain("Do not call create_goal or set_goal again") + expect(contentOf(duplicate)).toContain('"tokenBudget": 100') + expect(await readFile(process.env.OPENCODE_GOAL_STATE_PATH!, "utf8")).toBe(before) + const conflict = await goalTool(mock, "create_goal").execute({ objective: "replace V2 goal" }, toolContext()) + expect(contentOf(conflict)).toContain('"goal_conflict": true') + expect(contentOf(conflict)).toContain("Do not call create_goal or set_goal again") + expect(await readFile(process.env.OPENCODE_GOAL_STATE_PATH!, "utf8")).toBe(before) + + mock.stream.end() + await cleanup() +}) + test("V2 setup registers the /goal command via command transform", async () => { const mock = makeMockContext({ auto_continue: false }) const cleanup = await plugin.setup(mock as never) @@ -248,6 +275,8 @@ test("V2 setup registers the /goal command via command transform", async () => { expect(command).toBeDefined() expect(command?.template).toContain('OpenCode goal mode command "/goal" was invoked') expect(command?.template).toContain("$ARGUMENTS") + expect(command?.template).toContain("call get_goal first") + expect(command?.template).toContain("never call it again") mock.stream.end() await cleanup() diff --git a/test/server.test.ts b/test/server.test.ts index 10f431b..b1df3fb 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path" import { tmpdir } from "node:os" import plugin from "../src/server" import { + accountUsage, getGoal, getGoalInternal, recordContinuationResult, @@ -119,6 +120,93 @@ test("set goal lets the agent formulate the goal objective", async () => { expect(String(created)).toContain("audit the repo") }) +test("create_goal reuses the same active objective without mutating state", async () => { + const hooks = await plugin.server( + { client: { session: { promptAsync: async () => {} } } } as never, + { auto_continue: false }, + ) + const tools = hooks.tool! + const context = { sessionID: "ses_1" } as never + await requireTool(tools.create_goal, "create_goal").execute( + { objective: "finish safely", token_budget: 100 }, + context, + ) + const before = await readFile(process.env.OPENCODE_GOAL_STATE_PATH!, "utf8") + + const duplicate = await requireTool(tools.create_goal, "create_goal").execute( + { objective: " finish safely ", token_budget: 999 }, + context, + ) + + expect(String(duplicate)).toContain('"goal_reused": true') + expect(String(duplicate)).toContain("Do not call create_goal or set_goal again") + expect(String(duplicate)).toContain('"tokenBudget": 100') + expect(await readFile(process.env.OPENCODE_GOAL_STATE_PATH!, "utf8")).toBe(before) + const conflict = await requireTool(tools.create_goal, "create_goal").execute({ objective: "replace it" }, context) + expect(String(conflict)).toContain('"goal_conflict": true') + expect(String(conflict)).toContain("Do not call create_goal or set_goal again") + expect(await readFile(process.env.OPENCODE_GOAL_STATE_PATH!, "utf8")).toBe(before) + await expect( + requireTool(tools.create_goal, "create_goal").execute({ objective: " " }, context), + ).rejects.toThrow("must not be empty") + await expect( + requireTool(tools.create_goal, "create_goal").execute({ objective: "x".repeat(4_001) }, context), + ).rejects.toThrow("at most 4000 characters") +}) + +test("create_goal starts a fresh goal when the matching prior goal is closed", async () => { + const hooks = await plugin.server( + { client: { session: { promptAsync: async () => {} } } } as never, + { auto_continue: false }, + ) + const tools = hooks.tool! + const context = { sessionID: "ses_1" } as never + await requireTool(tools.create_goal, "create_goal").execute({ objective: "repeatable task" }, context) + await requireTool(tools.update_goal, "update_goal").execute( + { status: "complete", evidence: "first run verified" }, + context, + ) + + const created = await requireTool(tools.create_goal, "create_goal").execute({ objective: "repeatable task" }, context) + + expect(String(created)).toContain('"status": "active"') + expect(String(created)).not.toContain('"goal_reused"') + expect(String(created)).not.toContain("first run verified") +}) + +test("concurrent matching create_goal calls converge on one goal", async () => { + const hooks = await plugin.server( + { client: { session: { promptAsync: async () => {} } } } as never, + { auto_continue: false }, + ) + const create = requireTool(hooks.tool?.create_goal, "create_goal") + const context = { sessionID: "ses_1" } as never + + const results = await Promise.all([ + create.execute({ objective: "race safely" }, context), + create.execute({ objective: "race safely" }, context), + ]) + + expect(results.filter((result) => String(result).includes('"goal_reused": true'))).toHaveLength(1) + expect((await getGoal("ses_1"))?.history.filter((entry) => entry.type === "created")).toHaveLength(1) +}) + +test("duplicate limited goals retain the safety stop notice", async () => { + const hooks = await plugin.server( + { client: { session: { promptAsync: async () => {} } } } as never, + { auto_continue: false }, + ) + const tools = hooks.tool! + const context = { sessionID: "ses_1" } as never + await requireTool(tools.create_goal, "create_goal").execute({ objective: "bounded task", token_budget: 10 }, context) + await accountUsage("ses_1", 12) + + const duplicate = await requireTool(tools.create_goal, "create_goal").execute({ objective: "bounded task" }, context) + + expect(String(duplicate)).toContain('"status": "budgetLimited"') + expect(String(duplicate)).toContain("Safety limit reached") +}) + test("server plugin registers goal as a desktop/web command by default", async () => { const hooks = await plugin.server( { @@ -144,6 +232,9 @@ test("server plugin registers goal as a desktop/web command by default", async ( expect(config.command?.goal?.template).toContain("token_budget") expect(config.command?.goal?.template).toContain('"history"') expect(config.command?.goal?.template).toContain('"edit "') + expect(config.command?.goal?.template).toContain("call get_goal first") + expect(config.command?.goal?.template).toContain("call create_goal once") + expect(config.command?.goal?.template).toContain("never call it again") }) test("system transform is byte-stable across the complete goal lifecycle", async () => { @@ -1498,6 +1589,13 @@ test("create_goal from the plan agent records a paused goal", async () => { expect(String(created)).toContain('"status": "paused"') expect(String(created)).toContain('"plan_mode_notice"') + + const duplicate = await requireTool(tools.create_goal, "create_goal").execute( + { objective: "implement the feature" }, + { sessionID: "ses_1", agent: "build" } as never, + ) + expect(String(duplicate)).toContain('"goal_reused": true') + expect(String(duplicate)).toContain("paused for Plan mode") }) test("plan-created goal cannot resume from plan but resumes from build", async () => {