oh-my-opencode-slim 2.0.0-beta.13 → 2.0.0-beta.14

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.
package/dist/index.js CHANGED
@@ -18196,6 +18196,13 @@ function getCustomOpenCodeConfigDir() {
18196
18196
  const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
18197
18197
  return configDir || undefined;
18198
18198
  }
18199
+ function getConfigDir() {
18200
+ const customConfigDir = getCustomOpenCodeConfigDir();
18201
+ if (customConfigDir) {
18202
+ return customConfigDir;
18203
+ }
18204
+ return getDefaultOpenCodeConfigDir();
18205
+ }
18199
18206
  function getConfigSearchDirs() {
18200
18207
  const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
18201
18208
  return dirs.filter((dir, index) => {
@@ -18203,7 +18210,7 @@ function getConfigSearchDirs() {
18203
18210
  });
18204
18211
  }
18205
18212
  function getOpenCodeConfigPaths() {
18206
- const configDir = getDefaultOpenCodeConfigDir();
18213
+ const configDir = getConfigDir();
18207
18214
  return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
18208
18215
  }
18209
18216
 
@@ -18232,6 +18239,12 @@ var CUSTOM_SKILLS = [
18232
18239
  description: "Heavy/complex coding sessions and large modifications workflow",
18233
18240
  allowedAgents: ["orchestrator"],
18234
18241
  sourcePath: "src/skills/deepwork"
18242
+ },
18243
+ {
18244
+ name: "oh-my-opencode-slim",
18245
+ description: "Configure, customize, and safely improve oh-my-opencode-slim setups",
18246
+ allowedAgents: ["orchestrator"],
18247
+ sourcePath: "src/skills/oh-my-opencode-slim"
18235
18248
  }
18236
18249
  ];
18237
18250
 
@@ -18597,12 +18610,6 @@ var DivoomConfigSchema = z2.object({
18597
18610
  posterizeBits: z2.number().int().min(1).max(8).default(3),
18598
18611
  gifs: z2.record(z2.string(), z2.string().min(1)).optional()
18599
18612
  });
18600
- var TodoContinuationConfigSchema = z2.object({
18601
- maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
18602
- cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
18603
- autoEnable: z2.boolean().default(false).describe("Automatically enable auto-continue when the orchestrator session has enough todos"),
18604
- autoEnableThreshold: z2.number().int().min(1).max(50).default(4).describe("Number of todos that triggers auto-enable (only used when autoEnable is true)")
18605
- });
18606
18613
  var FailoverConfigSchema = z2.object({
18607
18614
  enabled: z2.boolean().default(true),
18608
18615
  timeoutMs: z2.number().min(0).default(15000),
@@ -18649,7 +18656,6 @@ var PluginConfigSchema = z2.object({
18649
18656
  interview: InterviewConfigSchema.optional(),
18650
18657
  backgroundJobs: BackgroundJobsConfigSchema.optional(),
18651
18658
  divoom: DivoomConfigSchema.optional(),
18652
- todoContinuation: TodoContinuationConfigSchema.optional(),
18653
18659
  fallback: FailoverConfigSchema.optional(),
18654
18660
  council: CouncilConfigSchema.optional()
18655
18661
  }).superRefine((value, ctx) => {
@@ -18903,34 +18909,32 @@ function parseModelReference(model) {
18903
18909
  async function promptWithTimeout(client, args, timeoutMs, signal) {
18904
18910
  if (signal?.aborted)
18905
18911
  throw new Error("Prompt cancelled");
18906
- if (timeoutMs <= 0) {
18907
- await client.session.prompt(args);
18908
- return;
18909
- }
18910
18912
  const sessionId = args.path.id;
18913
+ const hasTimeout = timeoutMs > 0;
18911
18914
  let timer;
18912
18915
  let onAbort;
18913
18916
  try {
18914
18917
  const promptPromise = client.session.prompt(args);
18915
18918
  promptPromise.catch(() => {});
18916
- await Promise.race([
18917
- promptPromise,
18918
- new Promise((_, reject) => {
18919
+ const racers = [promptPromise];
18920
+ if (hasTimeout) {
18921
+ racers.push(new Promise((_, reject) => {
18919
18922
  timer = setTimeout(() => {
18920
18923
  reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
18921
18924
  }, timeoutMs);
18922
- }),
18923
- new Promise((_, reject) => {
18924
- if (!signal)
18925
- return;
18925
+ }));
18926
+ }
18927
+ if (signal) {
18928
+ racers.push(new Promise((_, reject) => {
18926
18929
  if (signal.aborted) {
18927
18930
  reject(new Error("Prompt cancelled"));
18928
18931
  return;
18929
18932
  }
18930
18933
  onAbort = () => reject(new Error("Prompt cancelled"));
18931
18934
  signal.addEventListener("abort", onAbort, { once: true });
18932
- })
18933
- ]);
18935
+ }));
18936
+ }
18937
+ await Promise.race(racers);
18934
18938
  } catch (error) {
18935
18939
  if (error instanceof OperationTimeoutError) {
18936
18940
  try {
@@ -19161,13 +19165,6 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
19161
19165
  - If multiple remembered sessions fit, prefer the most recently used matching session.
19162
19166
  - Prefer re-uses over creating new sessions all the time
19163
19167
 
19164
- ### Auto-Continue
19165
- When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
19166
- - **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
19167
- - **Don't enable when:** User is in an interactive/conversational flow, or each step needs explicit review
19168
- - Use the \`auto_continue\` tool with \`enabled: true\` to activate. The system will automatically resume you when incomplete todos remain after you stop.
19169
- - The user can toggle this anytime via the \`/auto-continue\` command.
19170
-
19171
19168
  ### Validation routing
19172
19169
  - Validation is a workflow stage owned by the Orchestrator, not a separate specialist
19173
19170
  ${enabledValidationRouting}
@@ -20039,10 +20036,6 @@ function setActiveRuntimePresetWithPrevious(name) {
20039
20036
  previousRuntimePreset = activeRuntimePreset;
20040
20037
  activeRuntimePreset = name;
20041
20038
  }
20042
- function rollbackRuntimePreset(previous) {
20043
- activeRuntimePreset = previous;
20044
- previousRuntimePreset = null;
20045
- }
20046
20039
 
20047
20040
  // src/utils/logger.ts
20048
20041
  import * as fs2 from "node:fs";
@@ -20055,7 +20048,7 @@ var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
20055
20048
  var logFile = null;
20056
20049
  var writeChain = Promise.resolve();
20057
20050
  function getLogDir() {
20058
- return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
20051
+ return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode/log");
20059
20052
  }
20060
20053
  function cleanupOldLogs(logDir) {
20061
20054
  try {
@@ -24642,715 +24635,6 @@ function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
24642
24635
  ].join(`
24643
24636
  `);
24644
24637
  }
24645
- // src/hooks/todo-continuation/index.ts
24646
- import { tool } from "@opencode-ai/plugin";
24647
-
24648
- // src/hooks/todo-continuation/todo-hygiene.ts
24649
- var TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
24650
- var TODO_FINAL_ACTIVE_REMINDER = "If you are finishing now, do not leave the active todo in_progress. Mark it completed, or move unfinished work back to pending.";
24651
- var RESET = new Set(["todowrite"]);
24652
- var IGNORE = new Set(["auto_continue"]);
24653
- function createTodoHygiene(options) {
24654
- const pending = new Map;
24655
- const active = new Set;
24656
- function clearCycle(sessionID) {
24657
- pending.delete(sessionID);
24658
- }
24659
- function clear(sessionID) {
24660
- clearCycle(sessionID);
24661
- active.delete(sessionID);
24662
- }
24663
- function isFinalActive(state) {
24664
- return state.inProgressCount === 1 && state.pendingCount === 0 && state.openCount === 1;
24665
- }
24666
- function mark(sessionID, reason) {
24667
- const reasons = pending.get(sessionID) ?? new Set;
24668
- reasons.add(reason);
24669
- pending.set(sessionID, reasons);
24670
- }
24671
- function pick(reasons) {
24672
- if (reasons.has("final_active")) {
24673
- return TODO_FINAL_ACTIVE_REMINDER;
24674
- }
24675
- return TODO_HYGIENE_REMINDER;
24676
- }
24677
- return {
24678
- handleRequestStart(input) {
24679
- clear(input.sessionID);
24680
- },
24681
- async handleToolExecuteAfter(input, _output) {
24682
- if (!input.sessionID) {
24683
- return;
24684
- }
24685
- const tool = input.tool.toLowerCase();
24686
- if (IGNORE.has(tool)) {
24687
- return;
24688
- }
24689
- try {
24690
- if (RESET.has(tool)) {
24691
- if (options.shouldInject && !options.shouldInject(input.sessionID)) {
24692
- clear(input.sessionID);
24693
- return;
24694
- }
24695
- active.add(input.sessionID);
24696
- clearCycle(input.sessionID);
24697
- const state2 = await options.getTodoState(input.sessionID);
24698
- if (!state2.hasOpenTodos) {
24699
- active.delete(input.sessionID);
24700
- options.log?.("Cleared todo hygiene cycle", {
24701
- sessionID: input.sessionID,
24702
- tool
24703
- });
24704
- return;
24705
- }
24706
- if (!isFinalActive(state2)) {
24707
- options.log?.("Reset todo hygiene cycle", {
24708
- sessionID: input.sessionID,
24709
- tool
24710
- });
24711
- return;
24712
- }
24713
- mark(input.sessionID, "final_active");
24714
- options.log?.("Armed final-active todo hygiene reminder", {
24715
- sessionID: input.sessionID,
24716
- tool
24717
- });
24718
- return;
24719
- }
24720
- if (!active.has(input.sessionID)) {
24721
- return;
24722
- }
24723
- if (pending.get(input.sessionID)?.has("final_active")) {
24724
- return;
24725
- }
24726
- if (options.shouldInject && !options.shouldInject(input.sessionID)) {
24727
- clear(input.sessionID);
24728
- return;
24729
- }
24730
- const state = await options.getTodoState(input.sessionID);
24731
- if (!state.hasOpenTodos) {
24732
- clear(input.sessionID);
24733
- return;
24734
- }
24735
- if (isFinalActive(state)) {
24736
- mark(input.sessionID, "final_active");
24737
- } else {
24738
- mark(input.sessionID, "general");
24739
- }
24740
- options.log?.("Armed todo hygiene reminder", {
24741
- sessionID: input.sessionID,
24742
- tool,
24743
- reasons: Array.from(pending.get(input.sessionID) ?? [])
24744
- });
24745
- } catch (error) {
24746
- options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
24747
- sessionID: input.sessionID,
24748
- tool,
24749
- error: error instanceof Error ? error.message : String(error)
24750
- });
24751
- }
24752
- },
24753
- getPendingReminder(sessionID) {
24754
- const reasons = pending.get(sessionID);
24755
- if (!reasons || reasons.size === 0) {
24756
- return null;
24757
- }
24758
- if (options.shouldInject && !options.shouldInject(sessionID)) {
24759
- clear(sessionID);
24760
- return null;
24761
- }
24762
- const reminder = pick(reasons);
24763
- options.log?.("Read todo hygiene reminder", {
24764
- sessionID,
24765
- reminder,
24766
- reasons: Array.from(reasons)
24767
- });
24768
- return reminder;
24769
- },
24770
- handleEvent(event) {
24771
- if (event.type !== "session.deleted") {
24772
- return;
24773
- }
24774
- const sessionID = event.properties?.sessionID ?? event.properties?.info?.id;
24775
- if (!sessionID) {
24776
- return;
24777
- }
24778
- clear(sessionID);
24779
- }
24780
- };
24781
- }
24782
-
24783
- // src/hooks/todo-continuation/index.ts
24784
- var HOOK_NAME = "todo-continuation";
24785
- var COMMAND_NAME2 = "auto-continue";
24786
- var TODO_STATE_TIMEOUT_MS = 500;
24787
- var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
24788
- var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
24789
- var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
24790
- var SUPPRESS_AFTER_ABORT_MS = 5000;
24791
- var NOTIFICATION_BUSY_GRACE_MS = 250;
24792
- var QUESTION_PHRASES = [
24793
- "would you like",
24794
- "should i",
24795
- "do you want",
24796
- "please review",
24797
- "let me know",
24798
- "what do you think",
24799
- "can you confirm",
24800
- "would you prefer",
24801
- "shall i",
24802
- "any thoughts"
24803
- ];
24804
- var TERMINAL_TODO_STATUSES = ["completed", "cancelled"];
24805
- function isQuestion(text) {
24806
- const lowerText = text.toLowerCase().trim();
24807
- if (/\?\s*$/.test(lowerText)) {
24808
- return true;
24809
- }
24810
- return QUESTION_PHRASES.some((phrase) => lowerText.includes(phrase));
24811
- }
24812
- function cancelPendingTimer(state) {
24813
- if (state.pendingTimer) {
24814
- clearTimeout(state.pendingTimer);
24815
- state.pendingTimer = null;
24816
- }
24817
- state.pendingTimerSessionId = null;
24818
- }
24819
- function resetState(state) {
24820
- cancelPendingTimer(state);
24821
- state.consecutiveContinuations = 0;
24822
- state.suppressUntil = 0;
24823
- state.isAutoInjecting = false;
24824
- state.notifyingSessionIds.clear();
24825
- state.notificationBusyUntilBySession.clear();
24826
- }
24827
- function stripTodoHygieneInstruction(text) {
24828
- const trimmed = text.trimEnd();
24829
- if (!trimmed.endsWith(TODO_HYGIENE_INSTRUCTION_CLOSE)) {
24830
- return trimmed;
24831
- }
24832
- const start = trimmed.lastIndexOf(TODO_HYGIENE_INSTRUCTION_OPEN);
24833
- if (start === -1) {
24834
- return trimmed;
24835
- }
24836
- return trimmed.slice(0, start).trimEnd();
24837
- }
24838
- function appendTodoHygieneInstruction(message, reminder) {
24839
- const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
24840
- if (!textPart)
24841
- return;
24842
- const baseText = stripTodoHygieneInstruction(textPart.text ?? "");
24843
- const instruction = `${TODO_HYGIENE_INSTRUCTION_OPEN}
24844
- ${reminder}
24845
- ${TODO_HYGIENE_INSTRUCTION_CLOSE}`;
24846
- textPart.text = baseText ? `${baseText}
24847
-
24848
- ${instruction}` : instruction;
24849
- }
24850
- function stripTodoHygieneInstructionFromMessage(message) {
24851
- const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
24852
- if (!textPart)
24853
- return;
24854
- textPart.text = stripTodoHygieneInstruction(textPart.text ?? "");
24855
- }
24856
- function createTodoContinuationHook(ctx, config) {
24857
- const maxContinuations = config?.maxContinuations ?? 5;
24858
- const cooldownMs = config?.cooldownMs ?? 3000;
24859
- const autoEnable = config?.autoEnable ?? false;
24860
- const autoEnableThreshold = config?.autoEnableThreshold ?? 4;
24861
- const backgroundJobBoard = config?.backgroundJobBoard;
24862
- const requestSignatureBySession = new Map;
24863
- const state = {
24864
- enabled: false,
24865
- consecutiveContinuations: 0,
24866
- pendingTimer: null,
24867
- pendingTimerSessionId: null,
24868
- suppressUntil: 0,
24869
- orchestratorSessionIds: new Set,
24870
- sawChatMessage: false,
24871
- isAutoInjecting: false,
24872
- notifyingSessionIds: new Set,
24873
- notificationBusyUntilBySession: new Map
24874
- };
24875
- async function fetchTodos(sessionID) {
24876
- const result = await withTimeout(ctx.client.session.todo({
24877
- path: { id: sessionID }
24878
- }), TODO_STATE_TIMEOUT_MS, `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`);
24879
- return result.data;
24880
- }
24881
- const hygiene = createTodoHygiene({
24882
- getTodoState: async (sessionID) => {
24883
- const todos = await fetchTodos(sessionID);
24884
- const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
24885
- return {
24886
- hasOpenTodos: openTodos.length > 0,
24887
- openCount: openTodos.length,
24888
- inProgressCount: openTodos.filter((todo) => todo.status === "in_progress").length,
24889
- pendingCount: openTodos.filter((todo) => todo.status === "pending").length
24890
- };
24891
- },
24892
- shouldInject: (sessionID) => isOrchestratorSession(sessionID),
24893
- log: (message, meta) => log(`[${HOOK_NAME}] ${message}`, meta)
24894
- });
24895
- function inferSessionID(messages, index) {
24896
- const direct = messages[index]?.info.sessionID;
24897
- if (direct) {
24898
- return direct;
24899
- }
24900
- for (let i = index - 1;i >= 0; i--) {
24901
- const sessionID = messages[i]?.info.sessionID;
24902
- if (sessionID) {
24903
- return sessionID;
24904
- }
24905
- }
24906
- for (let i = index + 1;i < messages.length; i++) {
24907
- const sessionID = messages[i]?.info.sessionID;
24908
- if (sessionID) {
24909
- return sessionID;
24910
- }
24911
- }
24912
- if (state.orchestratorSessionIds.size === 1) {
24913
- return Array.from(state.orchestratorSessionIds)[0];
24914
- }
24915
- return;
24916
- }
24917
- function isExternalUserMessage(message) {
24918
- if (message.info.role !== "user") {
24919
- return false;
24920
- }
24921
- const visibleText = message.parts.filter((part) => part.type === "text" && typeof part.text === "string" && !part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER)).map((part) => part.text?.trim() ?? "").filter(Boolean).join(`
24922
- `);
24923
- const hasNonTextPart = message.parts.some((part) => part.type !== "text");
24924
- return !(!visibleText && !hasNonTextPart && message.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER)));
24925
- }
24926
- function getLastExternalUserMessage(messages) {
24927
- for (let i = messages.length - 1;i >= 0; i--) {
24928
- const message = messages[i];
24929
- if (!isExternalUserMessage(message)) {
24930
- continue;
24931
- }
24932
- const sessionID = inferSessionID(messages, i);
24933
- const partSignature = message.parts.map((part) => {
24934
- if (part.type === "text" && typeof part.text === "string") {
24935
- const text = stripTodoHygieneInstruction(part.text);
24936
- return `${part.type}:${text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? "<internal>" : text.trim()}`;
24937
- }
24938
- return part.type ?? "unknown";
24939
- }).join("|");
24940
- const ordinal = messages.slice(0, i + 1).filter((item) => isExternalUserMessage(item)).length;
24941
- return {
24942
- sessionID,
24943
- agent: message.info.agent,
24944
- message,
24945
- signature: message.info.id ? `${message.info.id}:${partSignature}` : `${ordinal}:${partSignature}`
24946
- };
24947
- }
24948
- return null;
24949
- }
24950
- async function handleMessagesTransform(output) {
24951
- const lastUserMessage = getLastExternalUserMessage(output.messages);
24952
- if (!lastUserMessage) {
24953
- return;
24954
- }
24955
- if (lastUserMessage.agent && lastUserMessage.agent !== "orchestrator") {
24956
- return;
24957
- }
24958
- if (!lastUserMessage.sessionID) {
24959
- for (const sessionID of state.orchestratorSessionIds) {
24960
- requestSignatureBySession.delete(sessionID);
24961
- hygiene.handleRequestStart({ sessionID });
24962
- }
24963
- return;
24964
- }
24965
- const knownOrchestrator = isOrchestratorSession(lastUserMessage.sessionID);
24966
- if (lastUserMessage.agent === "orchestrator") {
24967
- registerOrchestratorSession(lastUserMessage.sessionID);
24968
- } else if (!knownOrchestrator) {
24969
- return;
24970
- }
24971
- if (requestSignatureBySession.get(lastUserMessage.sessionID) === lastUserMessage.signature) {
24972
- const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
24973
- const guardrail = backgroundGuardrail(lastUserMessage.sessionID);
24974
- const combinedReminder = [reminder, guardrail].filter((item) => Boolean(item)).join(" ");
24975
- if (combinedReminder) {
24976
- appendTodoHygieneInstruction(lastUserMessage.message, combinedReminder);
24977
- } else {
24978
- stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
24979
- }
24980
- return;
24981
- }
24982
- requestSignatureBySession.set(lastUserMessage.sessionID, lastUserMessage.signature);
24983
- stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
24984
- hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
24985
- }
24986
- function markNotificationStarted(sessionID) {
24987
- state.notifyingSessionIds.add(sessionID);
24988
- }
24989
- function markNotificationFinished(sessionID) {
24990
- state.notifyingSessionIds.delete(sessionID);
24991
- state.notificationBusyUntilBySession.set(sessionID, Date.now() + NOTIFICATION_BUSY_GRACE_MS);
24992
- }
24993
- function clearNotificationState(sessionID) {
24994
- state.notifyingSessionIds.delete(sessionID);
24995
- state.notificationBusyUntilBySession.delete(sessionID);
24996
- }
24997
- function isNotificationBusy(sessionID) {
24998
- if (state.notifyingSessionIds.has(sessionID)) {
24999
- return true;
25000
- }
25001
- const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
25002
- if (until <= Date.now()) {
25003
- state.notificationBusyUntilBySession.delete(sessionID);
25004
- return false;
25005
- }
25006
- return true;
25007
- }
25008
- function isOrchestratorSession(sessionID) {
25009
- return state.orchestratorSessionIds.has(sessionID);
25010
- }
25011
- function registerOrchestratorSession(sessionID) {
25012
- state.orchestratorSessionIds.add(sessionID);
25013
- }
25014
- function backgroundGuardrail(sessionID) {
25015
- if (!backgroundJobBoard)
25016
- return;
25017
- const hasRunning = backgroundJobBoard.hasRunning(sessionID);
25018
- const hasTerminal = backgroundJobBoard.hasTerminalUnreconciled(sessionID);
25019
- if (hasRunning && hasTerminal) {
25020
- return "Background jobs are still unresolved: call task_status for running jobs and reconcile terminal Background Job Board results before dependent work or finalizing.";
25021
- }
25022
- if (hasTerminal) {
25023
- return "Background jobs have terminal results: reconcile the Background Job Board results before finalizing.";
25024
- }
25025
- if (hasRunning) {
25026
- return "Background jobs are still running: call task_status before dependent work or finalizing.";
25027
- }
25028
- return;
25029
- }
25030
- function continuationPrompt(sessionID) {
25031
- const guardrail = backgroundGuardrail(sessionID);
25032
- if (!guardrail)
25033
- return CONTINUATION_PROMPT;
25034
- return `${CONTINUATION_PROMPT} ${guardrail}`;
25035
- }
25036
- function handleChatMessage(input) {
25037
- if (!input.agent) {
25038
- return;
25039
- }
25040
- state.sawChatMessage = true;
25041
- if (input.agent === "orchestrator") {
25042
- registerOrchestratorSession(input.sessionID);
25043
- }
25044
- }
25045
- const autoContinue = tool({
25046
- description: "Toggle auto-continuation for incomplete todos. When enabled, the orchestrator will automatically continue working through its todo list when it stops with incomplete items.",
25047
- args: { enabled: tool.schema.boolean() },
25048
- execute: async (args) => {
25049
- const enabled = args.enabled;
25050
- state.enabled = enabled;
25051
- state.consecutiveContinuations = 0;
25052
- if (enabled) {
25053
- state.suppressUntil = 0;
25054
- log(`[${HOOK_NAME}] Auto-continue enabled`, { maxContinuations });
25055
- return `Auto-continue enabled. Will auto-continue for up to ${maxContinuations} consecutive injections.`;
25056
- }
25057
- cancelPendingTimer(state);
25058
- log(`[${HOOK_NAME}] Auto-continue disabled`);
25059
- return "Auto-continue disabled.";
25060
- }
25061
- });
25062
- async function handleEvent(input) {
25063
- const { event } = input;
25064
- const properties = event.properties ?? {};
25065
- hygiene.handleEvent({
25066
- type: event.type,
25067
- properties: {
25068
- info: properties.info,
25069
- sessionID: properties.sessionID
25070
- }
25071
- });
25072
- if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
25073
- const sessionID = properties.sessionID;
25074
- if (!sessionID) {
25075
- return;
25076
- }
25077
- log(`[${HOOK_NAME}] Session idle`, { sessionID });
25078
- if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
25079
- registerOrchestratorSession(sessionID);
25080
- log(`[${HOOK_NAME}] Tracked orchestrator session`, {
25081
- sessionID
25082
- });
25083
- }
25084
- if (!isOrchestratorSession(sessionID)) {
25085
- log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
25086
- sessionID
25087
- });
25088
- return;
25089
- }
25090
- if (autoEnable && !state.enabled) {
25091
- try {
25092
- const todos = await fetchTodos(sessionID);
25093
- const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
25094
- if (incompleteCount2 >= autoEnableThreshold) {
25095
- state.enabled = true;
25096
- state.consecutiveContinuations = 0;
25097
- state.suppressUntil = 0;
25098
- log(`[${HOOK_NAME}] Auto-enabled: ${incompleteCount2} incomplete todos >= threshold ${autoEnableThreshold}`, { sessionID });
25099
- } else {
25100
- log(`[${HOOK_NAME}] Auto-enable skipped: ${incompleteCount2} incomplete todos < threshold ${autoEnableThreshold}`, { sessionID });
25101
- }
25102
- } catch (error) {
25103
- log(`[${HOOK_NAME}] Warning: failed to fetch todos for auto-enable check`, {
25104
- sessionID,
25105
- error: error instanceof Error ? error.message : String(error)
25106
- });
25107
- }
25108
- }
25109
- if (!state.enabled) {
25110
- log(`[${HOOK_NAME}] Skipped: auto-continue not enabled`, {
25111
- sessionID
25112
- });
25113
- return;
25114
- }
25115
- let hasIncompleteTodos = false;
25116
- let incompleteCount = 0;
25117
- try {
25118
- const todos = await fetchTodos(sessionID);
25119
- incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
25120
- hasIncompleteTodos = incompleteCount > 0;
25121
- log(`[${HOOK_NAME}] Fetched todos`, {
25122
- sessionID,
25123
- hasIncompleteTodos,
25124
- total: todos.length
25125
- });
25126
- } catch (error) {
25127
- log(`[${HOOK_NAME}] Warning: failed to fetch todos`, {
25128
- sessionID,
25129
- error: error instanceof Error ? error.message : String(error)
25130
- });
25131
- return;
25132
- }
25133
- if (!hasIncompleteTodos) {
25134
- log(`[${HOOK_NAME}] Skipped: no incomplete todos`, { sessionID });
25135
- return;
25136
- }
25137
- let lastAssistantIsQuestion = false;
25138
- try {
25139
- const messagesResult = await ctx.client.session.messages({
25140
- path: { id: sessionID }
25141
- });
25142
- const messages = messagesResult.data;
25143
- const lastAssistantMessage = messages.slice().reverse().find((m) => m.info?.role === "assistant");
25144
- if (lastAssistantMessage?.parts) {
25145
- const lastText = lastAssistantMessage.parts.map((p) => p.text ?? "").join(" ");
25146
- lastAssistantIsQuestion = isQuestion(lastText);
25147
- }
25148
- log(`[${HOOK_NAME}] Fetched messages`, {
25149
- sessionID,
25150
- lastAssistantIsQuestion
25151
- });
25152
- } catch (error) {
25153
- log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
25154
- sessionID,
25155
- error: error instanceof Error ? error.message : String(error)
25156
- });
25157
- return;
25158
- }
25159
- if (lastAssistantIsQuestion) {
25160
- log(`[${HOOK_NAME}] Skipped: last message is question`, {
25161
- sessionID
25162
- });
25163
- return;
25164
- }
25165
- if (state.consecutiveContinuations >= maxContinuations) {
25166
- log(`[${HOOK_NAME}] Skipped: max continuations reached`, {
25167
- sessionID,
25168
- consecutive: state.consecutiveContinuations,
25169
- max: maxContinuations
25170
- });
25171
- return;
25172
- }
25173
- const now = Date.now();
25174
- if (now < state.suppressUntil) {
25175
- log(`[${HOOK_NAME}] Skipped: in suppress window`, {
25176
- sessionID,
25177
- suppressUntil: state.suppressUntil
25178
- });
25179
- return;
25180
- }
25181
- if (state.pendingTimer !== null || state.isAutoInjecting) {
25182
- log(`[${HOOK_NAME}] Skipped: timer pending or injection in flight`, {
25183
- sessionID
25184
- });
25185
- return;
25186
- }
25187
- log(`[${HOOK_NAME}] Scheduling continuation`, {
25188
- sessionID,
25189
- delayMs: cooldownMs
25190
- });
25191
- markNotificationStarted(sessionID);
25192
- ctx.client.session.prompt({
25193
- path: { id: sessionID },
25194
- body: {
25195
- noReply: true,
25196
- parts: [
25197
- {
25198
- type: "text",
25199
- text: [
25200
- `⎔ Auto-continue: ${incompleteCount} incomplete todos remaining — resuming in ${cooldownMs / 1000}s — Esc×2 to cancel`,
25201
- "",
25202
- "[system status: continue without acknowledging this notification]"
25203
- ].join(`
25204
- `)
25205
- }
25206
- ]
25207
- }
25208
- }).catch(() => {}).finally(() => {
25209
- markNotificationFinished(sessionID);
25210
- });
25211
- state.pendingTimerSessionId = sessionID;
25212
- state.pendingTimer = setTimeout(async () => {
25213
- state.pendingTimer = null;
25214
- state.pendingTimerSessionId = null;
25215
- clearNotificationState(sessionID);
25216
- if (!state.enabled) {
25217
- log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
25218
- sessionID
25219
- });
25220
- return;
25221
- }
25222
- state.isAutoInjecting = true;
25223
- try {
25224
- await ctx.client.session.prompt({
25225
- path: { id: sessionID },
25226
- body: {
25227
- parts: [
25228
- createInternalAgentTextPart(continuationPrompt(sessionID))
25229
- ]
25230
- }
25231
- });
25232
- state.consecutiveContinuations++;
25233
- log(`[${HOOK_NAME}] Continuation injected`, {
25234
- sessionID,
25235
- consecutive: state.consecutiveContinuations
25236
- });
25237
- } catch (error) {
25238
- log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
25239
- sessionID,
25240
- error: error instanceof Error ? error.message : String(error)
25241
- });
25242
- } finally {
25243
- state.isAutoInjecting = false;
25244
- }
25245
- }, cooldownMs);
25246
- } else if (event.type === "session.status") {
25247
- const status = properties.status;
25248
- const sessionID = properties.sessionID;
25249
- if (status?.type === "busy") {
25250
- const isOrchestrator = isOrchestratorSession(sessionID);
25251
- const isNotification = isNotificationBusy(sessionID);
25252
- if (isOrchestrator && !isNotification && state.pendingTimerSessionId === sessionID) {
25253
- cancelPendingTimer(state);
25254
- }
25255
- if (!state.isAutoInjecting && !isNotification && isOrchestrator && state.consecutiveContinuations > 0) {
25256
- state.consecutiveContinuations = 0;
25257
- log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
25258
- sessionID
25259
- });
25260
- }
25261
- }
25262
- } else if (event.type === "session.error") {
25263
- const error = properties.error;
25264
- const sessionID = properties.sessionID;
25265
- const errorName = error?.name;
25266
- const isOrchestrator = isOrchestratorSession(sessionID);
25267
- if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
25268
- state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
25269
- log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
25270
- sessionID,
25271
- errorName
25272
- });
25273
- }
25274
- if (isOrchestrator) {
25275
- cancelPendingTimer(state);
25276
- log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
25277
- sessionID
25278
- });
25279
- }
25280
- } else if (event.type === "session.deleted") {
25281
- const deletedSessionId = properties.info?.id ?? properties.sessionID;
25282
- if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
25283
- requestSignatureBySession.delete(deletedSessionId);
25284
- if (state.pendingTimerSessionId === deletedSessionId) {
25285
- cancelPendingTimer(state);
25286
- log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
25287
- sessionID: deletedSessionId
25288
- });
25289
- }
25290
- state.orchestratorSessionIds.delete(deletedSessionId);
25291
- clearNotificationState(deletedSessionId);
25292
- if (state.orchestratorSessionIds.size === 0) {
25293
- resetState(state);
25294
- state.sawChatMessage = false;
25295
- }
25296
- log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
25297
- sessionID: deletedSessionId
25298
- });
25299
- }
25300
- }
25301
- }
25302
- async function handleCommandExecuteBefore(input, output) {
25303
- if (input.command !== COMMAND_NAME2) {
25304
- return;
25305
- }
25306
- registerOrchestratorSession(input.sessionID);
25307
- output.parts.length = 0;
25308
- const arg = input.arguments.trim().toLowerCase();
25309
- let newEnabled;
25310
- if (arg === "on") {
25311
- newEnabled = true;
25312
- } else if (arg === "off") {
25313
- newEnabled = false;
25314
- } else {
25315
- newEnabled = !state.enabled;
25316
- }
25317
- state.enabled = newEnabled;
25318
- state.consecutiveContinuations = 0;
25319
- if (!newEnabled) {
25320
- cancelPendingTimer(state);
25321
- output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
25322
- log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME2} command`);
25323
- return;
25324
- }
25325
- state.suppressUntil = 0;
25326
- log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME2} command`, {
25327
- maxContinuations
25328
- });
25329
- let hasIncompleteTodos = false;
25330
- try {
25331
- const todos = await fetchTodos(input.sessionID);
25332
- hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
25333
- } catch (error) {
25334
- log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
25335
- sessionID: input.sessionID,
25336
- error: error instanceof Error ? error.message : String(error)
25337
- });
25338
- }
25339
- if (hasIncompleteTodos) {
25340
- output.parts.push(createInternalAgentTextPart(`${continuationPrompt(input.sessionID)} [Auto-continue enabled: up to ${maxContinuations} continuations.]`));
25341
- } else {
25342
- output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
25343
- }
25344
- }
25345
- return {
25346
- tool: { auto_continue: autoContinue },
25347
- handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
25348
- handleMessagesTransform,
25349
- handleEvent,
25350
- handleChatMessage,
25351
- handleCommandExecuteBefore
25352
- };
25353
- }
25354
24638
  // src/interview/manager.ts
25355
24639
  import path13 from "node:path";
25356
24640
 
@@ -28338,7 +27622,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
28338
27622
  }
28339
27623
 
28340
27624
  // src/interview/service.ts
28341
- var COMMAND_NAME3 = "interview";
27625
+ var COMMAND_NAME2 = "interview";
28342
27626
  var DEFAULT_MAX_QUESTIONS = 2;
28343
27627
  function isTruthyEnvFlag(value) {
28344
27628
  if (!value) {
@@ -28585,11 +27869,11 @@ function createInterviewService(ctx, config, deps) {
28585
27869
  }
28586
27870
  function registerCommand(opencodeConfig) {
28587
27871
  const configCommand = opencodeConfig.command;
28588
- if (!configCommand?.[COMMAND_NAME3]) {
27872
+ if (!configCommand?.[COMMAND_NAME2]) {
28589
27873
  if (!opencodeConfig.command) {
28590
27874
  opencodeConfig.command = {};
28591
27875
  }
28592
- opencodeConfig.command[COMMAND_NAME3] = {
27876
+ opencodeConfig.command[COMMAND_NAME2] = {
28593
27877
  template: "Start an interview and write a live markdown spec",
28594
27878
  description: "Open a localhost interview UI linked to the current OpenCode session"
28595
27879
  };
@@ -28663,7 +27947,7 @@ function createInterviewService(ctx, config, deps) {
28663
27947
  }
28664
27948
  }
28665
27949
  async function handleCommandExecuteBefore(input, output) {
28666
- if (input.command !== COMMAND_NAME3) {
27950
+ if (input.command !== COMMAND_NAME2) {
28667
27951
  return;
28668
27952
  }
28669
27953
  const idea = input.arguments.trim();
@@ -30320,7 +29604,7 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
30320
29604
  return false;
30321
29605
  }
30322
29606
  // src/tools/ast-grep/tools.ts
30323
- import { tool as tool2 } from "@opencode-ai/plugin";
29607
+ import { tool } from "@opencode-ai/plugin";
30324
29608
 
30325
29609
  // src/tools/ast-grep/cli.ts
30326
29610
  import { existsSync as existsSync9 } from "node:fs";
@@ -30799,14 +30083,14 @@ function showOutputToUser(context, output) {
30799
30083
  const ctx = context;
30800
30084
  ctx.metadata?.({ metadata: { output } });
30801
30085
  }
30802
- var ast_grep_search = tool2({
30086
+ var ast_grep_search = tool({
30803
30087
  description: "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + "IMPORTANT: Patterns must be complete AST nodes (valid code). " + "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
30804
30088
  args: {
30805
- pattern: tool2.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
30806
- lang: tool2.schema.enum(CLI_LANGUAGES).describe("Target language"),
30807
- paths: tool2.schema.array(tool2.schema.string()).optional().describe("Paths to search (default: ['.'])"),
30808
- globs: tool2.schema.array(tool2.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
30809
- context: tool2.schema.number().optional().describe("Context lines around match")
30089
+ pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
30090
+ lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
30091
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
30092
+ globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
30093
+ context: tool.schema.number().optional().describe("Context lines around match")
30810
30094
  },
30811
30095
  execute: async (args, context) => {
30812
30096
  try {
@@ -30835,15 +30119,15 @@ ${hint}`;
30835
30119
  }
30836
30120
  }
30837
30121
  });
30838
- var ast_grep_replace = tool2({
30122
+ var ast_grep_replace = tool({
30839
30123
  description: "Replace code patterns across filesystem with AST-aware rewriting. " + "Dry-run by default. Use meta-variables in rewrite to preserve matched content. " + "Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
30840
30124
  args: {
30841
- pattern: tool2.schema.string().describe("AST pattern to match"),
30842
- rewrite: tool2.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
30843
- lang: tool2.schema.enum(CLI_LANGUAGES).describe("Target language"),
30844
- paths: tool2.schema.array(tool2.schema.string()).optional().describe("Paths to search"),
30845
- globs: tool2.schema.array(tool2.schema.string()).optional().describe("Include/exclude globs"),
30846
- dryRun: tool2.schema.boolean().optional().describe("Preview changes without applying (default: true)")
30125
+ pattern: tool.schema.string().describe("AST pattern to match"),
30126
+ rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
30127
+ lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
30128
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
30129
+ globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"),
30130
+ dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)")
30847
30131
  },
30848
30132
  execute: async (args, context) => {
30849
30133
  try {
@@ -30867,14 +30151,14 @@ var ast_grep_replace = tool2({
30867
30151
  });
30868
30152
  // src/tools/cancel-task.ts
30869
30153
  import {
30870
- tool as tool3
30154
+ tool as tool2
30871
30155
  } from "@opencode-ai/plugin";
30872
- var z4 = tool3.schema;
30156
+ var z4 = tool2.schema;
30873
30157
 
30874
30158
  class SessionStillRunningError extends Error {
30875
30159
  }
30876
30160
  function createCancelTaskTool(options) {
30877
- const cancel_task = tool3({
30161
+ const cancel_task = tool2({
30878
30162
  description: `Cancel a tracked background specialist task.
30879
30163
 
30880
30164
  Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accepts either the native task_id/session ID or the parent-scoped alias shown in the Background Job Board. Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before replacing the lane.`,
@@ -31264,9 +30548,9 @@ function unknownTaskOutput(taskID, message) {
31264
30548
  }
31265
30549
  // src/tools/council.ts
31266
30550
  import {
31267
- tool as tool4
30551
+ tool as tool3
31268
30552
  } from "@opencode-ai/plugin";
31269
- var z5 = tool4.schema;
30553
+ var z5 = tool3.schema;
31270
30554
  function formatModelComposition(councillorResults) {
31271
30555
  return councillorResults.map((cr) => {
31272
30556
  const shortModel = shortModelLabel(cr.model);
@@ -31274,7 +30558,7 @@ function formatModelComposition(councillorResults) {
31274
30558
  }).join(", ");
31275
30559
  }
31276
30560
  function createCouncilTool(_ctx, councilManager) {
31277
- const council_session = tool4({
30561
+ const council_session = tool3({
31278
30562
  description: `Launch a multi-LLM council session for consensus-based analysis.
31279
30563
 
31280
30564
  Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
@@ -31328,6 +30612,9 @@ Returns the councillor responses with a summary footer.`,
31328
30612
  });
31329
30613
  return { council_session };
31330
30614
  }
30615
+ // src/tools/preset-manager.ts
30616
+ import * as fs10 from "node:fs";
30617
+
31331
30618
  // src/tui-state.ts
31332
30619
  import * as fs9 from "node:fs";
31333
30620
  import * as os5 from "node:os";
@@ -31397,11 +30684,11 @@ function recordTuiAgentModel(input) {
31397
30684
  }
31398
30685
 
31399
30686
  // src/tools/preset-manager.ts
31400
- var COMMAND_NAME4 = "preset";
30687
+ var COMMAND_NAME3 = "preset";
31401
30688
  function createPresetManager(ctx, config) {
31402
30689
  let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
31403
30690
  async function handleCommandExecuteBefore(input, output) {
31404
- if (input.command !== COMMAND_NAME4) {
30691
+ if (input.command !== COMMAND_NAME3) {
31405
30692
  return;
31406
30693
  }
31407
30694
  output.parts.length = 0;
@@ -31420,11 +30707,11 @@ function createPresetManager(ctx, config) {
31420
30707
  }
31421
30708
  function registerCommand(opencodeConfig) {
31422
30709
  const configCommand = opencodeConfig.command;
31423
- if (!configCommand?.[COMMAND_NAME4]) {
30710
+ if (!configCommand?.[COMMAND_NAME3]) {
31424
30711
  if (!opencodeConfig.command) {
31425
30712
  opencodeConfig.command = {};
31426
30713
  }
31427
- opencodeConfig.command[COMMAND_NAME4] = {
30714
+ opencodeConfig.command[COMMAND_NAME3] = {
31428
30715
  template: "List available presets and switch between them",
31429
30716
  description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
31430
30717
  };
@@ -31446,64 +30733,47 @@ function createPresetManager(ctx, config) {
31446
30733
  agentUpdates[resolvedName] = agentConfig;
31447
30734
  }
31448
30735
  }
31449
- const currentRuntimePreset = getActiveRuntimePreset();
31450
- const resetUpdates = {};
31451
- if (currentRuntimePreset && config.presets?.[currentRuntimePreset]) {
31452
- const oldPreset = config.presets[currentRuntimePreset];
31453
- for (const rawName of Object.keys(oldPreset)) {
31454
- const resolvedOld = AGENT_ALIASES[rawName] ?? rawName;
31455
- if (resolvedOld in agentUpdates)
31456
- continue;
31457
- const baseline = config.agents?.[resolvedOld];
31458
- if (baseline) {
31459
- resetUpdates[resolvedOld] = mapOverrideToAgentConfig(baseline);
31460
- }
31461
- }
31462
- }
31463
30736
  const hasAgentUpdates = Object.keys(agentUpdates).length > 0;
31464
- const allUpdates = { ...resetUpdates, ...agentUpdates };
31465
30737
  if (!hasAgentUpdates) {
31466
30738
  output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
31467
30739
  return;
31468
30740
  }
31469
- const previousPreset = activePreset;
31470
30741
  setActiveRuntimePresetWithPrevious(presetName);
31471
30742
  try {
31472
- await ctx.client.config.update({
31473
- body: { agent: allUpdates }
31474
- });
31475
- const snapshot = readTuiSnapshot();
31476
- const agentModels = { ...snapshot.agentModels };
31477
- for (const [agentName, agentConfig] of Object.entries(allUpdates)) {
31478
- if (typeof agentConfig.model === "string") {
31479
- agentModels[agentName] = agentConfig.model;
31480
- }
31481
- }
31482
- recordTuiAgentModels({ agentModels });
31483
- activePreset = presetName;
31484
- const summaryParts = [];
31485
- for (const [name, cfg] of Object.entries(agentUpdates)) {
31486
- const parts = [name];
31487
- if (cfg.model)
31488
- parts.push(`model: ${cfg.model}`);
31489
- if (cfg.variant)
31490
- parts.push(`variant: ${cfg.variant}`);
31491
- if (cfg.temperature !== undefined)
31492
- parts.push(`temp: ${cfg.temperature}`);
31493
- if (cfg.options)
31494
- parts.push("options: yes");
31495
- summaryParts.push(parts.join(" → "));
31496
- }
31497
- if (Object.keys(resetUpdates).length > 0) {
31498
- summaryParts.push(`Reset to baseline: ${Object.keys(resetUpdates).join(", ")}`);
31499
- }
31500
- output.parts.push(createInternalAgentTextPart(`Switched to preset "${presetName}":
30743
+ const { userConfigPath } = findPluginConfigPaths(ctx.directory);
30744
+ if (userConfigPath) {
30745
+ const raw = fs10.readFileSync(userConfigPath, "utf-8");
30746
+ const persisted = JSON.parse(stripJsonComments(raw));
30747
+ persisted.preset = presetName;
30748
+ fs10.writeFileSync(userConfigPath, `${JSON.stringify(persisted, null, 2)}
30749
+ `);
30750
+ }
30751
+ } catch {}
30752
+ const snapshot = readTuiSnapshot();
30753
+ const agentModels = { ...snapshot.agentModels };
30754
+ for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
30755
+ if (typeof agentConfig.model === "string") {
30756
+ agentModels[agentName] = agentConfig.model;
30757
+ }
30758
+ }
30759
+ recordTuiAgentModels({ agentModels });
30760
+ activePreset = presetName;
30761
+ const summaryParts = [];
30762
+ for (const [name, cfg] of Object.entries(agentUpdates)) {
30763
+ const parts = [name];
30764
+ if (cfg.model)
30765
+ parts.push(`model: ${cfg.model}`);
30766
+ if (cfg.variant)
30767
+ parts.push(`variant: ${cfg.variant}`);
30768
+ if (cfg.temperature !== undefined)
30769
+ parts.push(`temp: ${cfg.temperature}`);
30770
+ if (cfg.options)
30771
+ parts.push("options: yes");
30772
+ summaryParts.push(parts.join(" → "));
30773
+ }
30774
+ output.parts.push(createInternalAgentTextPart(`Saved preset "${presetName}". Restart or reload OpenCode to apply it to agent configuration. The current session was not reloaded to avoid interrupting the active conversation.
31501
30775
  ${summaryParts.join(`
31502
30776
  `)}`));
31503
- } catch (err) {
31504
- rollbackRuntimePreset(previousPreset);
31505
- output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
31506
- }
31507
30777
  }
31508
30778
  function mapOverrideToAgentConfig(override) {
31509
30779
  const agentConfig = {};
@@ -31593,7 +30863,7 @@ var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs p
31593
30863
  import os6 from "node:os";
31594
30864
  import path18 from "node:path";
31595
30865
  import {
31596
- tool as tool5
30866
+ tool as tool4
31597
30867
  } from "@opencode-ai/plugin";
31598
30868
 
31599
30869
  // src/tools/smartfetch/binary.ts
@@ -33375,10 +32645,10 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
33375
32645
  }
33376
32646
 
33377
32647
  // src/tools/smartfetch/tool.ts
33378
- var z6 = tool5.schema;
32648
+ var z6 = tool4.schema;
33379
32649
  function createWebfetchTool(pluginCtx, options = {}) {
33380
32650
  const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
33381
- return tool5({
32651
+ return tool4({
33382
32652
  description: WEBFETCH_DESCRIPTION,
33383
32653
  args: {
33384
32654
  url: z6.httpUrl(),
@@ -33969,7 +33239,6 @@ var OhMyOpenCodeLite = async (ctx) => {
33969
33239
  let applyPatchHook;
33970
33240
  let jsonErrorRecoveryHook;
33971
33241
  let foregroundFallback;
33972
- let todoContinuationHook;
33973
33242
  let deepworkCommandHook;
33974
33243
  let taskSessionManagerHook;
33975
33244
  let backgroundJobBoard;
@@ -34062,13 +33331,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34062
33331
  applyPatchHook = createApplyPatchHook(ctx);
34063
33332
  jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
34064
33333
  foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
34065
- todoContinuationHook = createTodoContinuationHook(ctx, {
34066
- maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
34067
- cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
34068
- autoEnable: config.todoContinuation?.autoEnable ?? false,
34069
- autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
34070
- backgroundJobBoard
34071
- });
34072
33334
  deepworkCommandHook = createDeepworkCommandHook();
34073
33335
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
34074
33336
  maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
@@ -34085,7 +33347,7 @@ var OhMyOpenCodeLite = async (ctx) => {
34085
33347
  backgroundJobBoard,
34086
33348
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
34087
33349
  });
34088
- toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
33350
+ toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + 1 + 2;
34089
33351
  } catch (err) {
34090
33352
  log("[plugin] FATAL: init failed", String(err));
34091
33353
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -34129,7 +33391,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34129
33391
  ...councilTools,
34130
33392
  ...cancelTaskTools,
34131
33393
  webfetch,
34132
- ...todoContinuationHook.tool,
34133
33394
  ast_grep_search,
34134
33395
  ast_grep_replace
34135
33396
  },
@@ -34316,16 +33577,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34316
33577
  }
34317
33578
  agentConfigEntry.permission = agentPermission;
34318
33579
  }
34319
- const configCommand = opencodeConfig.command;
34320
- if (!configCommand?.["auto-continue"]) {
34321
- if (!opencodeConfig.command) {
34322
- opencodeConfig.command = {};
34323
- }
34324
- opencodeConfig.command["auto-continue"] = {
34325
- template: "Call the auto_continue tool with enabled=true",
34326
- description: "Enable auto-continuation — orchestrator keeps working through incomplete todos"
34327
- };
34328
- }
34329
33580
  interviewManager.registerCommand(opencodeConfig);
34330
33581
  deepworkCommandHook.registerCommand(opencodeConfig);
34331
33582
  presetManager.registerCommand(opencodeConfig);
@@ -34352,7 +33603,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34352
33603
  await multiplexerSessionManager.onSessionStatus(event);
34353
33604
  await multiplexerSessionManager.onSessionDeleted(event);
34354
33605
  await foregroundFallback.handleEvent(input.event);
34355
- await todoContinuationHook.handleEvent(input);
34356
33606
  await autoUpdateChecker.event(input);
34357
33607
  await interviewManager.handleEvent(input);
34358
33608
  await taskSessionManagerHook.event(input);
@@ -34410,7 +33660,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34410
33660
  }
34411
33661
  },
34412
33662
  "command.execute.before": async (input, output) => {
34413
- await todoContinuationHook.handleCommandExecuteBefore(input, output);
34414
33663
  await interviewManager.handleCommandExecuteBefore(input, output);
34415
33664
  await presetManager.handleCommandExecuteBefore(input, output);
34416
33665
  await deepworkCommandHook.handleCommandExecuteBefore(input, output);
@@ -34425,10 +33674,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34425
33674
  if (agent) {
34426
33675
  sessionAgentMap.set(input.sessionID, agent);
34427
33676
  }
34428
- todoContinuationHook.handleChatMessage({
34429
- sessionID: input.sessionID,
34430
- agent
34431
- });
34432
33677
  },
34433
33678
  "experimental.chat.system.transform": async (input, output) => {
34434
33679
  const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
@@ -34463,9 +33708,6 @@ ${output.system[0]}` : "");
34463
33708
  disabledAgents,
34464
33709
  log
34465
33710
  });
34466
- await todoContinuationHook.handleMessagesTransform({
34467
- messages: typedOutput.messages
34468
- });
34469
33711
  await taskSessionManagerHook["experimental.chat.messages.transform"](input, typedOutput);
34470
33712
  await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
34471
33713
  await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
@@ -34487,7 +33729,6 @@ ${output.system[0]}` : "");
34487
33729
  };
34488
33730
  await runPostToolHook("delegate-task-retry", () => delegateTaskRetryHook["tool.execute.after"](input, output));
34489
33731
  await runPostToolHook("json-error-recovery", () => jsonErrorRecoveryHook["tool.execute.after"](input, output));
34490
- await runPostToolHook("todo-continuation", () => todoContinuationHook.handleToolExecuteAfter(input, output));
34491
33732
  await runPostToolHook("post-file-tool-nudge", () => postFileToolNudgeHook["tool.execute.after"](input, output));
34492
33733
  await runPostToolHook("task-session-manager", () => taskSessionManagerHook["tool.execute.after"](input, output));
34493
33734
  if (input.tool.toLowerCase() === "task") {