@prevalentware/opencode-goal-plugin 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/server.js +162 -27
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -152,12 +152,20 @@ var PendingAttemptSchema = Schema.Struct({
152
152
  armNoProgress: Schema.Boolean,
153
153
  previousLastContinuationAt: Schema.NullOr(Schema.Number)
154
154
  });
155
+ var UsageTrackerSchema = Schema.Struct({
156
+ baseline: Schema.optionalWith(Schema.Unknown, { default: () => null }),
157
+ lastObserved: Schema.optionalWith(Schema.Unknown, { default: () => null }),
158
+ baseTokens: Schema.optionalWith(Schema.Unknown, { default: () => null }),
159
+ pendingBaseline: Schema.optionalWith(Schema.Unknown, { default: () => null }),
160
+ pendingBaseTokens: Schema.optionalWith(Schema.Unknown, { default: () => null })
161
+ });
155
162
  var GoalSchema = Schema.Struct({
156
163
  sessionID: Schema.String,
157
164
  objective: Schema.String,
158
165
  status: Schema.Literal("active", "paused", "budgetLimited", "usageLimited", "complete", "unmet"),
159
166
  tokenBudget: NullableNumber,
160
167
  tokensUsed: Schema.Number,
168
+ usageTrackers: Schema.optionalWith(Schema.Record({ key: Schema.String, value: UsageTrackerSchema }), { default: () => ({}) }),
161
169
  timeUsedSeconds: Schema.Number,
162
170
  createdAt: Schema.Number,
163
171
  updatedAt: Schema.Number,
@@ -315,12 +323,34 @@ function normalizeGoal(goal) {
315
323
  goal.maxAutoTurns = positiveIntegerOrNull(goal.maxAutoTurns);
316
324
  goal.maxDurationSeconds = positiveIntegerOrNull(goal.maxDurationSeconds);
317
325
  goal.tokenBudget = positiveIntegerOrNull(goal.tokenBudget);
326
+ goal.usageTrackers = normalizeUsageTrackers(goal.usageTrackers);
318
327
  goal.noProgressTokenThreshold = positiveIntegerOrNull(goal.noProgressTokenThreshold) ?? DEFAULT_NO_PROGRESS_TOKEN_THRESHOLD;
319
328
  goal.maxNoProgressTurns = positiveIntegerOrNull(goal.maxNoProgressTurns) ?? DEFAULT_MAX_NO_PROGRESS_TURNS;
320
329
  goal.budgetWrapupSent = goal.budgetWrapupSent === true;
321
330
  goal.stopReason ??= null;
322
331
  return goal;
323
332
  }
333
+ function normalizeUsageTrackers(trackers) {
334
+ const normalized = {};
335
+ for (const [source, rawTracker] of Object.entries(trackers ?? {})) {
336
+ const tracker = rawTracker;
337
+ const baseline = nonNegativeIntegerOrNull(tracker?.baseline);
338
+ const lastObserved = nonNegativeIntegerOrNull(tracker?.lastObserved);
339
+ const baseTokens = nonNegativeIntegerOrNull(tracker?.baseTokens);
340
+ if (source && baseline != null && lastObserved != null && baseTokens != null && lastObserved >= baseline) {
341
+ const pendingBaseline = nonNegativeIntegerOrNull(tracker.pendingBaseline);
342
+ const pendingBaseTokens = nonNegativeIntegerOrNull(tracker.pendingBaseTokens);
343
+ normalized[source] = {
344
+ baseline,
345
+ lastObserved,
346
+ baseTokens,
347
+ pendingBaseline,
348
+ pendingBaseTokens: pendingBaseline == null ? null : pendingBaseTokens
349
+ };
350
+ }
351
+ }
352
+ return normalized;
353
+ }
324
354
  function normalizePendingAttempt(attempt) {
325
355
  if (!attempt || typeof attempt !== "object")
326
356
  return null;
@@ -365,6 +395,9 @@ function positiveIntegerOrNull(value) {
365
395
  function nonNegativeInteger(value, fallback) {
366
396
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
367
397
  }
398
+ function nonNegativeIntegerOrNull(value) {
399
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
400
+ }
368
401
  function isClosed(status) {
369
402
  return status === "complete" || status === "unmet";
370
403
  }
@@ -444,6 +477,7 @@ async function createGoal(sessionID, objective, options) {
444
477
  status: normalizedOptions.initialStatus,
445
478
  tokenBudget: normalizedOptions.tokenBudget,
446
479
  tokensUsed: 0,
480
+ usageTrackers: {},
447
481
  timeUsedSeconds: 0,
448
482
  createdAt: now,
449
483
  updatedAt: now,
@@ -605,14 +639,66 @@ async function clearGoal(sessionID) {
605
639
  return existed;
606
640
  });
607
641
  }
608
- async function accountUsage(sessionID, tokensUsed) {
642
+ async function accountUsage(sessionID, tokensUsed, options) {
609
643
  return mutate((state) => {
610
644
  const goal = state.goals[sessionID];
611
645
  if (!goal)
612
646
  return null;
613
647
  accountWallClock(goal);
614
648
  if (typeof tokensUsed === "number" && Number.isFinite(tokensUsed)) {
615
- goal.tokensUsed = Math.max(goal.tokensUsed, Math.max(0, Math.ceil(tokensUsed)));
649
+ const observed = Math.max(0, Math.ceil(tokensUsed));
650
+ if (options?.cumulative === true) {
651
+ const source = options.source?.trim() || "default";
652
+ let tracker = goal.usageTrackers[source];
653
+ if (!tracker) {
654
+ const initialBaseline = nonNegativeIntegerOrNull(options.initialBaseline);
655
+ tracker = initialBaseline != null && initialBaseline <= observed ? {
656
+ baseline: initialBaseline,
657
+ lastObserved: observed,
658
+ baseTokens: goal.tokensUsed,
659
+ pendingBaseline: null,
660
+ pendingBaseTokens: null
661
+ } : {
662
+ baseline: observed,
663
+ lastObserved: observed,
664
+ baseTokens: goal.tokensUsed,
665
+ pendingBaseline: null,
666
+ pendingBaseTokens: null
667
+ };
668
+ goal.usageTrackers[source] = tracker;
669
+ } else if (observed < tracker.lastObserved) {
670
+ const initialBaseline = nonNegativeIntegerOrNull(options.initialBaseline);
671
+ if (initialBaseline != null && initialBaseline <= observed) {
672
+ tracker = {
673
+ baseline: initialBaseline,
674
+ lastObserved: observed,
675
+ baseTokens: goal.tokensUsed,
676
+ pendingBaseline: null,
677
+ pendingBaseTokens: null
678
+ };
679
+ goal.usageTrackers[source] = tracker;
680
+ } else if (tracker.pendingBaseline == null || observed < tracker.pendingBaseline) {
681
+ tracker.pendingBaseline = observed;
682
+ tracker.pendingBaseTokens = goal.tokensUsed;
683
+ } else {
684
+ tracker = {
685
+ baseline: tracker.pendingBaseline,
686
+ lastObserved: observed,
687
+ baseTokens: tracker.pendingBaseTokens ?? goal.tokensUsed,
688
+ pendingBaseline: null,
689
+ pendingBaseTokens: null
690
+ };
691
+ goal.usageTrackers[source] = tracker;
692
+ }
693
+ } else {
694
+ tracker.lastObserved = observed;
695
+ tracker.pendingBaseline = null;
696
+ tracker.pendingBaseTokens = null;
697
+ }
698
+ goal.tokensUsed = Math.max(goal.tokensUsed, tracker.baseTokens + observed - tracker.baseline);
699
+ } else {
700
+ goal.tokensUsed = Math.max(goal.tokensUsed, observed);
701
+ }
616
702
  }
617
703
  maybeStopForBudget(goal);
618
704
  goal.updatedAt = nowSeconds();
@@ -1058,6 +1144,9 @@ var NON_PROGRESS_TOOLS = new Set(["get_goal", "get_goal_history"]);
1058
1144
  var TASK_TERMINAL_STATES = new Set(["completed", "error", "cancelled"]);
1059
1145
  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.';
1060
1146
  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.";
1147
+ 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.";
1148
+ 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.";
1149
+ 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.";
1061
1150
  var activeContinuations = new Set;
1062
1151
  function restrictedAgentSet(options) {
1063
1152
  if (options?.allow_goal_execution_from_plan === true)
@@ -1084,9 +1173,9 @@ Use the goal tools to handle this command:
1084
1173
  - If the arguments start with "edit ", update the current goal objective by calling update_goal_objective with the remaining text.
1085
1174
  - 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.
1086
1175
  - 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.
1087
- - 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.
1176
+ - 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.
1088
1177
 
1089
- 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.`;
1178
+ 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.`;
1090
1179
  }
1091
1180
  function commandNameFromOptions(options) {
1092
1181
  const name = options?.command_name?.trim() || DEFAULT_COMMAND_NAME;
@@ -1097,7 +1186,7 @@ function commandNameFromOptions(options) {
1097
1186
  function positiveIntegerOrNull2(value) {
1098
1187
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1099
1188
  }
1100
- function nonNegativeIntegerOrNull(value) {
1189
+ function nonNegativeIntegerOrNull2(value) {
1101
1190
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
1102
1191
  }
1103
1192
  function timeoutMillisecondsFromSeconds(value) {
@@ -1190,9 +1279,9 @@ function outputTokensFromMessage(message) {
1190
1279
  return outputTokensFromRecord(message.info.tokens);
1191
1280
  return;
1192
1281
  }
1193
- function tokensFromMessages(messages) {
1282
+ function usageFromMessages(messages) {
1194
1283
  const exactTotal = messages.reduce((sum, message) => sum + (exactTokensFromMessage(message) ?? 0), 0);
1195
- return exactTotal > 0 ? exactTotal : estimateMessages(messages);
1284
+ return exactTotal > 0 ? { tokens: exactTotal, source: "v1.messages.exact" } : { tokens: estimateMessages(messages), source: "v1.messages.estimated" };
1196
1285
  }
1197
1286
  function taskHeader(output) {
1198
1287
  const resultIndex = output.search(/<task_(?:result|error)>/);
@@ -1687,17 +1776,44 @@ function getGoalToolResult(goal) {
1687
1776
  }
1688
1777
  async function createGoalFromTool(input, context, services) {
1689
1778
  const planningOnly = services.isPlanAgent(context.agent);
1690
- const goal = await createGoal(context.sessionID, input.objective, {
1691
- tokenBudget: input.token_budget ?? services.options.default_token_budget ?? null,
1692
- maxAutoTurns: input.max_auto_turns ?? null,
1693
- maxDurationSeconds: input.max_duration_seconds ?? services.options.max_goal_duration_seconds ?? null,
1694
- noProgressTokenThreshold: services.options.no_progress_token_threshold ?? null,
1695
- maxNoProgressTurns: services.options.max_no_progress_turns ?? null,
1696
- agent: typeof context.agent === "string" ? context.agent : null,
1697
- initialStatus: planningOnly ? "paused" : "active"
1698
- });
1779
+ const objective = validateObjective(input.objective);
1780
+ const existing = await getGoal(context.sessionID);
1781
+ if (existing && !isClosedGoal(existing))
1782
+ return existingGoalResult(existing, objective, planningOnly);
1783
+ let goal;
1784
+ try {
1785
+ goal = await createGoal(context.sessionID, input.objective, {
1786
+ tokenBudget: input.token_budget ?? services.options.default_token_budget ?? null,
1787
+ maxAutoTurns: input.max_auto_turns ?? null,
1788
+ maxDurationSeconds: input.max_duration_seconds ?? services.options.max_goal_duration_seconds ?? null,
1789
+ noProgressTokenThreshold: services.options.no_progress_token_threshold ?? null,
1790
+ maxNoProgressTurns: services.options.max_no_progress_turns ?? null,
1791
+ agent: typeof context.agent === "string" ? context.agent : null,
1792
+ initialStatus: planningOnly ? "paused" : "active"
1793
+ });
1794
+ } catch (error) {
1795
+ if (!(error instanceof Error) || !error.message.includes("non-closed goal"))
1796
+ throw error;
1797
+ const raced = await getGoal(context.sessionID);
1798
+ if (raced && !isClosedGoal(raced))
1799
+ return existingGoalResult(raced, objective, planningOnly);
1800
+ throw error;
1801
+ }
1802
+ await services.initializeUsage?.(context.sessionID);
1699
1803
  return JSON.stringify(planningOnly ? { goal, plan_mode_notice: PLAN_MODE_CREATE_NOTICE } : { goal }, null, 2);
1700
1804
  }
1805
+ function isClosedGoal(goal) {
1806
+ return goal.status === "complete" || goal.status === "unmet";
1807
+ }
1808
+ function existingGoalResult(goal, requestedObjective, planningOnly) {
1809
+ const reused = goal.objective === requestedObjective;
1810
+ return JSON.stringify({
1811
+ goal,
1812
+ ...reused ? { goal_reused: true, duplicate_goal_notice: DUPLICATE_GOAL_NOTICE } : { goal_conflict: true, goal_conflict_notice: CONFLICTING_GOAL_NOTICE },
1813
+ ...goal.status === "budgetLimited" || goal.status === "usageLimited" ? { goal_mode_notice: LIMITED_GOAL_NOTICE } : {},
1814
+ ...planningOnly || goal.stopReason === PLAN_MODE_STOP_REASON ? { plan_mode_notice: RESTRICTED_GOAL_NOTICE } : {}
1815
+ }, null, 2);
1816
+ }
1701
1817
  async function updateGoalObjectiveFromTool(input, context, services) {
1702
1818
  const requested = input.status ?? "active";
1703
1819
  const planningOnly = requested === "active" && services.isPlanAgent(context.agent);
@@ -1758,7 +1874,7 @@ var server = async ({ client }, options) => {
1758
1874
  const autoContinue = options?.auto_continue ?? true;
1759
1875
  const deferWhileTasksActive = options?.defer_while_tasks_active ?? true;
1760
1876
  const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
1761
- const minInterval = nonNegativeIntegerOrNull(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
1877
+ const minInterval = nonNegativeIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
1762
1878
  const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
1763
1879
  const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
1764
1880
  const registerCommand = options?.register_command ?? true;
@@ -2056,7 +2172,7 @@ var server = async ({ client }, options) => {
2056
2172
  }
2057
2173
  },
2058
2174
  create_goal: {
2059
- 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.",
2175
+ 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.",
2060
2176
  args: {
2061
2177
  objective: z.string().min(1).max(4000).describe("The concrete objective to start pursuing."),
2062
2178
  token_budget: z.number().int().positive().nullable().optional().describe("Optional positive token budget."),
@@ -2068,7 +2184,7 @@ var server = async ({ client }, options) => {
2068
2184
  }
2069
2185
  },
2070
2186
  set_goal: {
2071
- 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.",
2187
+ 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.",
2072
2188
  args: {
2073
2189
  objective: z.string().min(1).max(4000).describe("The model-formulated concrete objective to start pursuing."),
2074
2190
  token_budget: z.number().int().positive().nullable().optional().describe("Optional positive token budget."),
@@ -2167,7 +2283,8 @@ var server = async ({ client }, options) => {
2167
2283
  const sessionID = "sessionID" in input && typeof input.sessionID === "string" ? input.sessionID : output.messages.find((message) => typeof message.info.sessionID === "string")?.info.sessionID;
2168
2284
  if (!sessionID)
2169
2285
  return;
2170
- await accountUsage(sessionID, tokensFromMessages(output.messages));
2286
+ const usage = usageFromMessages(output.messages);
2287
+ await accountUsage(sessionID, usage.tokens, { cumulative: true, source: usage.source });
2171
2288
  const observed = await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
2172
2289
  await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
2173
2290
  const scheduled = scheduledContinuations.get(sessionID);
@@ -2298,7 +2415,7 @@ async function setupV2(context) {
2298
2415
  const autoContinue = options.auto_continue ?? true;
2299
2416
  const deferWhileTasksActive = options.defer_while_tasks_active ?? true;
2300
2417
  const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
2301
- const minInterval = nonNegativeIntegerOrNull(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
2418
+ const minInterval = nonNegativeIntegerOrNull2(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
2302
2419
  const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
2303
2420
  const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
2304
2421
  const registerCommand = options.register_command ?? true;
@@ -2314,11 +2431,21 @@ async function setupV2(context) {
2314
2431
  const toolAttempts = new Map;
2315
2432
  const planAgents = restrictedAgentSet(options);
2316
2433
  const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
2317
- const goalServices = { options, isPlanAgent };
2318
2434
  const activeContinuationsV2 = new Set;
2319
2435
  const latestStepBySession = new Map;
2320
2436
  const stepTextBuffers = new Map;
2321
2437
  const stepTokenSums = new Map;
2438
+ const goalServices = {
2439
+ options,
2440
+ isPlanAgent,
2441
+ initializeUsage: async (sessionID) => {
2442
+ try {
2443
+ await accountUsage(sessionID, stepTokenSums.get(sessionID) ?? 0, { cumulative: true, source: "v2.steps" });
2444
+ } catch (error) {
2445
+ v2ErrorLog("Failed to initialize goal usage accounting", error);
2446
+ }
2447
+ }
2448
+ };
2322
2449
  const registrations = [];
2323
2450
  let disposed = false;
2324
2451
  function stepKey(sessionID, messageID2) {
@@ -2719,7 +2846,11 @@ async function setupV2(context) {
2719
2846
  if (typeof tokens === "number") {
2720
2847
  const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
2721
2848
  stepTokenSums.set(sessionID, sum);
2722
- await accountUsage(sessionID, sum);
2849
+ await accountUsage(sessionID, sum, {
2850
+ cumulative: true,
2851
+ source: "v2.steps",
2852
+ initialBaseline: Math.ceil(sum - tokens)
2853
+ });
2723
2854
  }
2724
2855
  const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
2725
2856
  stepTextBuffers.delete(stepKey(sessionID, messageID2));
@@ -2755,7 +2886,11 @@ async function setupV2(context) {
2755
2886
  if (typeof tokens === "number") {
2756
2887
  const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
2757
2888
  stepTokenSums.set(sessionID, sum);
2758
- await accountUsage(sessionID, sum);
2889
+ await accountUsage(sessionID, sum, {
2890
+ cumulative: true,
2891
+ source: "v2.steps",
2892
+ initialBaseline: Math.ceil(sum - tokens)
2893
+ });
2759
2894
  }
2760
2895
  const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
2761
2896
  stepTextBuffers.delete(stepKey(sessionID, messageID2));
@@ -2788,7 +2923,7 @@ async function setupV2(context) {
2788
2923
  return;
2789
2924
  const tokens = tokensFromRecord(data.tokens);
2790
2925
  if (typeof tokens === "number")
2791
- await accountUsage(sessionID, tokens);
2926
+ await accountUsage(sessionID, tokens, { cumulative: true, source: "v2.session" });
2792
2927
  return;
2793
2928
  }
2794
2929
  }
@@ -2913,7 +3048,7 @@ function goalToolsV2(services) {
2913
3048
  },
2914
3049
  {
2915
3050
  name: "create_goal",
2916
- 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.",
3051
+ 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.",
2917
3052
  input: v2ObjectSchema({
2918
3053
  objective: { type: "string", minLength: 1, maxLength: 4000, description: "The concrete objective to start pursuing." },
2919
3054
  token_budget: { type: ["integer", "null"], minimum: 1, description: "Optional positive token budget." },
@@ -2927,7 +3062,7 @@ function goalToolsV2(services) {
2927
3062
  },
2928
3063
  {
2929
3064
  name: "set_goal",
2930
- 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.",
3065
+ 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.",
2931
3066
  input: v2ObjectSchema({
2932
3067
  objective: {
2933
3068
  type: "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prevalentware/opencode-goal-plugin",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "OpenCode goal plugin that adds Codex-style long-running goal mode, /goal commands, persistence, and TUI status for AI coding agents.",
5
5
  "keywords": [
6
6
  "opencode",