oh-my-opencode-slim 2.0.0-beta.1 → 2.0.0-beta.3

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 (55) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/index.js +6 -0
  3. package/dist/hooks/deepwork/index.d.ts +13 -0
  4. package/dist/hooks/{session-goal → goal}/index.d.ts +1 -1
  5. package/dist/hooks/index.d.ts +2 -1
  6. package/dist/index.js +497 -923
  7. package/dist/tools/index.d.ts +0 -2
  8. package/dist/tui.js +6 -0
  9. package/package.json +3 -2
  10. package/src/skills/codemap.md +3 -2
  11. package/src/skills/deepwork/SKILL.md +92 -0
  12. package/dist/agents/council-master.d.ts +0 -2
  13. package/dist/background/background-manager.d.ts +0 -203
  14. package/dist/background/index.d.ts +0 -3
  15. package/dist/background/multiplexer-session-manager.d.ts +0 -70
  16. package/dist/background/subagent-depth.d.ts +0 -35
  17. package/dist/cli/divoom.d.ts +0 -23
  18. package/dist/goal/index.d.ts +0 -3
  19. package/dist/goal/manager.d.ts +0 -41
  20. package/dist/goal/prompts.d.ts +0 -4
  21. package/dist/goal/store.d.ts +0 -15
  22. package/dist/goal/types.d.ts +0 -28
  23. package/dist/integrations/divoom/index.d.ts +0 -3
  24. package/dist/integrations/divoom/status-manager.d.ts +0 -31
  25. package/dist/integrations/divoom/swift-helper-source.d.ts +0 -1
  26. package/dist/integrations/divoom/swift-transport.d.ts +0 -26
  27. package/dist/integrations/divoom/types.d.ts +0 -41
  28. package/dist/tools/background.d.ts +0 -13
  29. package/dist/tools/fork/command.d.ts +0 -28
  30. package/dist/tools/fork/files.d.ts +0 -33
  31. package/dist/tools/fork/index.d.ts +0 -10
  32. package/dist/tools/fork/state.d.ts +0 -7
  33. package/dist/tools/fork/tools.d.ts +0 -23
  34. package/dist/tools/fork/vendor.d.ts +0 -28
  35. package/dist/tools/handoff/command.d.ts +0 -29
  36. package/dist/tools/handoff/files.d.ts +0 -33
  37. package/dist/tools/handoff/index.d.ts +0 -10
  38. package/dist/tools/handoff/state.d.ts +0 -7
  39. package/dist/tools/handoff/tools.d.ts +0 -23
  40. package/dist/tools/handoff/vendor.d.ts +0 -28
  41. package/dist/tools/lsp/client.d.ts +0 -81
  42. package/dist/tools/lsp/config-store.d.ts +0 -29
  43. package/dist/tools/lsp/config.d.ts +0 -5
  44. package/dist/tools/lsp/constants.d.ts +0 -24
  45. package/dist/tools/lsp/index.d.ts +0 -4
  46. package/dist/tools/lsp/tools.d.ts +0 -5
  47. package/dist/tools/lsp/types.d.ts +0 -45
  48. package/dist/tools/lsp/utils.d.ts +0 -34
  49. package/dist/tools/subtask/command.d.ts +0 -30
  50. package/dist/tools/subtask/files.d.ts +0 -34
  51. package/dist/tools/subtask/index.d.ts +0 -11
  52. package/dist/tools/subtask/state.d.ts +0 -7
  53. package/dist/tools/subtask/tools.d.ts +0 -23
  54. package/dist/tools/subtask/vendor.d.ts +0 -27
  55. package/dist/utils/tmux-debug-log.d.ts +0 -2
package/dist/index.js CHANGED
@@ -18226,6 +18226,12 @@ var CUSTOM_SKILLS = [
18226
18226
  description: "Clone important dependency source for local inspection",
18227
18227
  allowedAgents: ["orchestrator"],
18228
18228
  sourcePath: "src/skills/clonedeps"
18229
+ },
18230
+ {
18231
+ name: "deepwork",
18232
+ description: "Heavy/complex coding sessions and large modifications workflow",
18233
+ allowedAgents: ["orchestrator"],
18234
+ sourcePath: "src/skills/deepwork"
18229
18235
  }
18230
18236
  ];
18231
18237
 
@@ -19101,27 +19107,6 @@ ${enabledParallelExamples}
19101
19107
 
19102
19108
  Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
19103
19109
 
19104
- ### Context Isolation
19105
- If no specialist delegation is needed, consider \`subtask\` before doing
19106
- context-heavy work directly.
19107
-
19108
- Ask whether the parent context needs the details or only the result. Use
19109
- \`subtask\` when the work is bounded, context-heavy, and the parent only needs a
19110
- compact outcome.
19111
-
19112
- Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
19113
- verification across files/logs/messages.
19114
-
19115
- Prefer native background \`task(..., background: true)\` plus \`task_status\` for independent specialist lanes. Use \`subtask\` only for bounded parent-local context isolation when native background specialist scheduling is not the right fit.
19116
-
19117
- Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
19118
- work better handled by a named specialist, or cases where the parent must reason
19119
- over the details.
19120
-
19121
- When calling \`subtask\`, give a self-contained prompt with objective,
19122
- constraints, relevant context, deliverable, and validation. Pass only clearly
19123
- relevant files. Wait for the summary, then integrate and verify it.
19124
-
19125
19110
  ### OpenCode scheduler model
19126
19111
  - Delegated specialists should be launched as background tasks whenever work can run independently: use \`task(..., background: true)\`.
19127
19112
  - A dispatch returns a task/session ID immediately; it does not mean completion.
@@ -23207,6 +23192,53 @@ function createChatHeadersHook(ctx) {
23207
23192
  }
23208
23193
  };
23209
23194
  }
23195
+ // src/hooks/deepwork/index.ts
23196
+ var COMMAND_NAME = "deepwork";
23197
+ function activationPrompt(task2) {
23198
+ return [
23199
+ "Use the deepwork skill for this task. Treat it as a heavy coding session.",
23200
+ "",
23201
+ "Deepwork requirements:",
23202
+ "- create/update a `.slim/deepwork/` progress file;",
23203
+ "- keep OpenCode todos synced with the current phase;",
23204
+ "- draft a plan and get `@oracle` review before implementation;",
23205
+ "- create and review a phased implementation/delegation plan;",
23206
+ "- execute phase by phase with background specialists where useful;",
23207
+ "- poll `task_status`, reconcile results, validate, and ask `@oracle` to review each phase;",
23208
+ "- ask `@oracle` to include simplify/readability feedback in phase reviews;",
23209
+ "- fix actionable review issues before continuing.",
23210
+ "",
23211
+ "Task:",
23212
+ task2
23213
+ ].join(`
23214
+ `);
23215
+ }
23216
+ function createDeepworkCommandHook() {
23217
+ return {
23218
+ registerCommand: (opencodeConfig) => {
23219
+ const commandConfig = opencodeConfig.command;
23220
+ if (commandConfig?.[COMMAND_NAME])
23221
+ return;
23222
+ if (!opencodeConfig.command)
23223
+ opencodeConfig.command = {};
23224
+ opencodeConfig.command[COMMAND_NAME] = {
23225
+ template: "Start a deepwork session for a complex coding task",
23226
+ description: "Use the deepwork workflow for heavy multi-phase coding work"
23227
+ };
23228
+ },
23229
+ handleCommandExecuteBefore: async (input, output) => {
23230
+ if (input.command !== COMMAND_NAME)
23231
+ return;
23232
+ output.parts.length = 0;
23233
+ const task2 = input.arguments.trim();
23234
+ if (!task2) {
23235
+ output.parts.push(createInternalAgentTextPart("What task should deepwork manage? Run `/deepwork <task>`."));
23236
+ return;
23237
+ }
23238
+ output.parts.push({ type: "text", text: activationPrompt(task2) });
23239
+ }
23240
+ };
23241
+ }
23210
23242
  // src/hooks/delegate-task-retry/patterns.ts
23211
23243
  var DELEGATE_TASK_ERROR_PATTERNS = [
23212
23244
  {
@@ -23620,370 +23652,58 @@ class ForegroundFallbackManager {
23620
23652
  return all;
23621
23653
  }
23622
23654
  }
23623
- // src/hooks/image-hook.ts
23624
- import { createHash } from "node:crypto";
23625
- import {
23626
- existsSync as existsSync5,
23627
- mkdirSync as mkdirSync3,
23628
- readdirSync as readdirSync2,
23629
- rmdirSync,
23630
- statSync as statSync3,
23631
- unlinkSync as unlinkSync2,
23632
- writeFileSync as writeFileSync3
23633
- } from "node:fs";
23634
- import { basename as basename2, extname, join as join7 } from "node:path";
23635
- var lastCleanupByDir = new Map;
23636
- var CLEANUP_INTERVAL = 10 * 60 * 1000;
23637
- function isImagePart(p) {
23638
- if (p.type === "image")
23639
- return true;
23640
- if (p.type === "file") {
23641
- const mime = p.mime;
23642
- if (mime?.startsWith("image/"))
23643
- return true;
23644
- const filename = p.filename;
23645
- const name = p.name;
23646
- const fileName = filename ?? name;
23647
- if (fileName && /\.(png|jpg|jpeg|gif|bmp|webp|svg|ico|tiff?|heic)$/i.test(fileName))
23648
- return true;
23649
- }
23650
- return false;
23651
- }
23652
- function decodeDataUrl(url) {
23653
- const match = url.match(/^data:([^;]+);base64,(.+)$/);
23654
- if (!match)
23655
- return null;
23656
- return { mime: match[1], data: Buffer.from(match[2], "base64") };
23655
+ // src/hooks/goal/index.ts
23656
+ import * as fs7 from "node:fs/promises";
23657
+
23658
+ // src/interview/document.ts
23659
+ import * as fsSync from "node:fs";
23660
+ import * as fs6 from "node:fs/promises";
23661
+ import * as path9 from "node:path";
23662
+ var DEFAULT_OUTPUT_FOLDER = "interview";
23663
+ function normalizeOutputFolder(outputFolder) {
23664
+ const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
23665
+ return normalized || DEFAULT_OUTPUT_FOLDER;
23657
23666
  }
23658
- function extFromMime(mime) {
23659
- const map = {
23660
- "image/png": ".png",
23661
- "image/jpeg": ".jpg",
23662
- "image/gif": ".gif",
23663
- "image/webp": ".webp",
23664
- "image/svg+xml": ".svg",
23665
- "image/bmp": ".bmp"
23666
- };
23667
- return map[mime] ?? ".png";
23667
+ function createInterviewDirectoryPath(directory, outputFolder) {
23668
+ return path9.join(directory, normalizeOutputFolder(outputFolder));
23668
23669
  }
23669
- function sanitizeFilename(name) {
23670
- return name.replace(/[^a-zA-Z0-9._-]/g, "_");
23670
+ function createInterviewFilePath(directory, outputFolder, idea) {
23671
+ const fileName = `${slugify(idea) || "interview"}.md`;
23672
+ return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
23671
23673
  }
23672
- function cleanupAllSessions(saveDir) {
23673
- const now = Date.now();
23674
- const lastCleanup = lastCleanupByDir.get(saveDir) ?? 0;
23675
- if (now - lastCleanup < CLEANUP_INTERVAL)
23676
- return;
23677
- lastCleanupByDir.set(saveDir, now);
23678
- const maxAge = 60 * 60 * 1000;
23679
- const dirsToScan = [];
23680
- try {
23681
- for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
23682
- const fp = join7(saveDir, entry.name);
23683
- if (entry.isDirectory()) {
23684
- dirsToScan.push(fp);
23685
- } else {
23686
- try {
23687
- if (now - statSync3(fp).mtimeMs > maxAge)
23688
- unlinkSync2(fp);
23689
- } catch {}
23690
- }
23691
- }
23692
- } catch {}
23693
- for (const dir of dirsToScan) {
23694
- try {
23695
- let isEmpty = true;
23696
- let allRemoved = true;
23697
- for (const f of readdirSync2(dir)) {
23698
- isEmpty = false;
23699
- const fp = join7(dir, f);
23700
- try {
23701
- if (now - statSync3(fp).mtimeMs > maxAge) {
23702
- unlinkSync2(fp);
23703
- } else {
23704
- allRemoved = false;
23705
- }
23706
- } catch {
23707
- allRemoved = false;
23708
- }
23709
- }
23710
- if (!isEmpty && allRemoved) {
23711
- try {
23712
- rmdirSync(dir);
23713
- } catch {}
23714
- }
23715
- } catch {}
23716
- }
23674
+ function relativeInterviewPath(directory, filePath) {
23675
+ return path9.relative(directory, filePath) || path9.basename(filePath);
23717
23676
  }
23718
- function writeUniqueFile(dir, name, data, log2) {
23719
- const ext = extname(name);
23720
- const base = basename2(name, ext) || name;
23721
- let candidate = join7(dir, name);
23722
- if (existsSync5(candidate)) {
23723
- return candidate;
23677
+ function resolveExistingInterviewPath(directory, outputFolder, value) {
23678
+ const trimmed = value.trim();
23679
+ if (!trimmed) {
23680
+ return null;
23724
23681
  }
23725
- let counter = 0;
23726
- const MAX_ATTEMPTS = 1000;
23727
- for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
23728
- try {
23729
- writeFileSync3(candidate, data, { flag: "wx" });
23730
- return candidate;
23731
- } catch (e) {
23732
- if (e instanceof Error && e.code === "EEXIST") {
23733
- counter += 1;
23734
- candidate = join7(dir, `${base}-${counter}${ext}`);
23735
- continue;
23736
- }
23737
- log2(`[image-hook] failed to save image: ${e}`);
23738
- return null;
23682
+ const outputDir = createInterviewDirectoryPath(directory, outputFolder);
23683
+ const candidates = new Set;
23684
+ const resolvedRoot = path9.resolve(directory);
23685
+ if (path9.isAbsolute(trimmed)) {
23686
+ candidates.add(trimmed);
23687
+ } else {
23688
+ candidates.add(path9.resolve(directory, trimmed));
23689
+ candidates.add(path9.join(outputDir, trimmed));
23690
+ if (!trimmed.endsWith(".md")) {
23691
+ candidates.add(path9.join(outputDir, `${trimmed}.md`));
23739
23692
  }
23740
23693
  }
23741
- log2(`[image-hook] failed to save image: max attempts (${MAX_ATTEMPTS}) reached`);
23742
- return null;
23743
- }
23744
- function processImageAttachments(args) {
23745
- const { messages, workDir, disabledAgents, log: log2 } = args;
23746
- const observerEnabled = !disabledAgents.has("observer");
23747
- if (!observerEnabled)
23748
- return;
23749
- const messagesWithImages = [];
23750
- for (const msg of messages) {
23751
- if (msg.info.role !== "user")
23694
+ for (const candidate of candidates) {
23695
+ if (path9.extname(candidate) !== ".md") {
23752
23696
  continue;
23753
- const imageParts = msg.parts.filter(isImagePart);
23754
- if (imageParts.length > 0) {
23755
- messagesWithImages.push({ msg, imageParts });
23756
23697
  }
23757
- }
23758
- const saveDir = join7(workDir, ".opencode", "images");
23759
- if (messagesWithImages.length === 0) {
23760
- if (existsSync5(saveDir))
23761
- cleanupAllSessions(saveDir);
23762
- return;
23763
- }
23764
- const gitignorePath = join7(workDir, ".opencode", ".gitignore");
23765
- try {
23766
- mkdirSync3(saveDir, { recursive: true });
23767
- if (!existsSync5(gitignorePath))
23768
- writeFileSync3(gitignorePath, `*
23769
- `);
23770
- } catch (e) {
23771
- log2(`[image-hook] failed to create image directory: ${e}`);
23772
- }
23773
- cleanupAllSessions(saveDir);
23774
- for (const { msg, imageParts } of messagesWithImages) {
23775
- const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
23776
- const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
23777
- try {
23778
- mkdirSync3(targetDir, { recursive: true });
23779
- } catch (e) {
23780
- log2(`[image-hook] failed to create target image directory: ${e}`);
23698
+ const resolved = path9.resolve(candidate);
23699
+ if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
23700
+ continue;
23781
23701
  }
23782
- const savedPaths = [];
23783
- for (const p of imageParts) {
23784
- const url = p.url;
23785
- const filename = p.filename ?? p.name;
23786
- if (url) {
23787
- const decoded = decodeDataUrl(url);
23788
- if (decoded) {
23789
- const hash = createHash("sha1").update(decoded.data).digest("hex").slice(0, 8);
23790
- const sanitizedFilename = filename ? sanitizeFilename(filename) : undefined;
23791
- const baseName = sanitizedFilename ? sanitizedFilename.replace(/\.[^.]+$/, "") || "image" : "image";
23792
- const ext = sanitizedFilename ? extname(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
23793
- const name = `${baseName}-${hash}${ext}`;
23794
- const filePath = writeUniqueFile(targetDir, name, decoded.data, log2);
23795
- if (filePath)
23796
- savedPaths.push(filePath);
23797
- }
23798
- }
23702
+ if (fsSync.existsSync(candidate)) {
23703
+ return candidate;
23799
23704
  }
23800
- const pathsText = savedPaths.length > 0 ? ` Saved to: ${savedPaths.join(", ")}` : "";
23801
- log2(`[image-hook] stripping image/file parts, saving to disk${pathsText}`);
23802
- msg.parts = msg.parts.filter((p) => !isImagePart(p)).concat([
23803
- {
23804
- type: "text",
23805
- text: `[Image attachment detected.${pathsText} Your model may not support image input. Delegate to @observer with the file path(s) above so it can read the file with its read tool.]`
23806
- }
23807
- ]);
23808
23705
  }
23809
- }
23810
- // src/hooks/json-error-recovery/hook.ts
23811
- var JSON_ERROR_TOOL_EXCLUDE_LIST = [
23812
- "bash",
23813
- "read",
23814
- "glob",
23815
- "webfetch",
23816
- "grep_app_searchgithub",
23817
- "websearch_web_search_exa"
23818
- ];
23819
- var JSON_ERROR_PATTERNS = [
23820
- /json parse error/i,
23821
- /failed to parse json/i,
23822
- /invalid json/i,
23823
- /malformed json/i,
23824
- /unexpected end of json input/i,
23825
- /syntaxerror:\s*unexpected token.*json/i,
23826
- /json[^\n]*expected '\}'/i,
23827
- /json[^\n]*unexpected eof/i
23828
- ];
23829
- var JSON_ERROR_REMINDER_MARKER = "[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]";
23830
- var JSON_ERROR_EXCLUDED_TOOLS = new Set(JSON_ERROR_TOOL_EXCLUDE_LIST);
23831
- var JSON_ERROR_REMINDER = `
23832
- [JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
23833
-
23834
- You sent invalid JSON arguments. The system could not parse your tool call.
23835
- STOP and do this NOW:
23836
-
23837
- 1. LOOK at the error message above to see what was expected vs what you sent.
23838
- 2. CORRECT your JSON syntax (missing braces, unescaped quotes, trailing commas, etc).
23839
- 3. RETRY the tool call with valid JSON.
23840
-
23841
- DO NOT repeat the exact same invalid call.
23842
- `;
23843
- function createJsonErrorRecoveryHook(_ctx) {
23844
- return {
23845
- "tool.execute.after": async (input, output) => {
23846
- if (JSON_ERROR_EXCLUDED_TOOLS.has(input.tool.toLowerCase()))
23847
- return;
23848
- if (typeof output.output !== "string")
23849
- return;
23850
- if (output.output.includes(JSON_ERROR_REMINDER_MARKER))
23851
- return;
23852
- const outputText = output.output;
23853
- const hasJsonError = JSON_ERROR_PATTERNS.some((pattern) => pattern.test(outputText));
23854
- if (hasJsonError) {
23855
- output.output += `
23856
- ${JSON_ERROR_REMINDER}`;
23857
- }
23858
- }
23859
- };
23860
- }
23861
- // src/hooks/phase-reminder/index.ts
23862
- var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
23863
- function createPhaseReminderHook() {
23864
- return {
23865
- "experimental.chat.messages.transform": async (_input, output) => {
23866
- const { messages } = output;
23867
- if (messages.length === 0) {
23868
- return;
23869
- }
23870
- let lastUserMessageIndex = -1;
23871
- for (let i = messages.length - 1;i >= 0; i--) {
23872
- if (messages[i].info.role === "user") {
23873
- lastUserMessageIndex = i;
23874
- break;
23875
- }
23876
- }
23877
- if (lastUserMessageIndex === -1) {
23878
- return;
23879
- }
23880
- const lastUserMessage = messages[lastUserMessageIndex];
23881
- const agent = lastUserMessage.info.agent;
23882
- if (agent && agent !== "orchestrator") {
23883
- return;
23884
- }
23885
- const textPartIndex = lastUserMessage.parts.findIndex((p) => p.type === "text" && p.text !== undefined);
23886
- if (textPartIndex === -1) {
23887
- return;
23888
- }
23889
- const originalText = lastUserMessage.parts[textPartIndex].text ?? "";
23890
- if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
23891
- return;
23892
- }
23893
- if (lastUserMessage.parts.some((p) => p.text?.includes(PHASE_REMINDER))) {
23894
- return;
23895
- }
23896
- lastUserMessage.parts.push({
23897
- type: "text",
23898
- text: PHASE_REMINDER
23899
- });
23900
- }
23901
- };
23902
- }
23903
- // src/hooks/post-file-tool-nudge/index.ts
23904
- var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
23905
- var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
23906
- function createPostFileToolNudgeHook(options = {}) {
23907
- function appendReminder(output) {
23908
- if (typeof output.output !== "string") {
23909
- return;
23910
- }
23911
- if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
23912
- return;
23913
- }
23914
- output.output = [
23915
- output.output,
23916
- "",
23917
- "<internal_reminder>",
23918
- POST_FILE_TOOL_NUDGE,
23919
- "</internal_reminder>"
23920
- ].join(`
23921
- `);
23922
- }
23923
- return {
23924
- "tool.execute.after": async (input, output) => {
23925
- if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
23926
- return;
23927
- }
23928
- if (options.shouldInject && !options.shouldInject(input.sessionID)) {
23929
- return;
23930
- }
23931
- appendReminder(output);
23932
- }
23933
- };
23934
- }
23935
- // src/hooks/session-goal/index.ts
23936
- import * as fs7 from "node:fs/promises";
23937
-
23938
- // src/interview/document.ts
23939
- import * as fsSync from "node:fs";
23940
- import * as fs6 from "node:fs/promises";
23941
- import * as path9 from "node:path";
23942
- var DEFAULT_OUTPUT_FOLDER = "interview";
23943
- function normalizeOutputFolder(outputFolder) {
23944
- const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
23945
- return normalized || DEFAULT_OUTPUT_FOLDER;
23946
- }
23947
- function createInterviewDirectoryPath(directory, outputFolder) {
23948
- return path9.join(directory, normalizeOutputFolder(outputFolder));
23949
- }
23950
- function createInterviewFilePath(directory, outputFolder, idea) {
23951
- const fileName = `${slugify(idea) || "interview"}.md`;
23952
- return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
23953
- }
23954
- function relativeInterviewPath(directory, filePath) {
23955
- return path9.relative(directory, filePath) || path9.basename(filePath);
23956
- }
23957
- function resolveExistingInterviewPath(directory, outputFolder, value) {
23958
- const trimmed = value.trim();
23959
- if (!trimmed) {
23960
- return null;
23961
- }
23962
- const outputDir = createInterviewDirectoryPath(directory, outputFolder);
23963
- const candidates = new Set;
23964
- const resolvedRoot = path9.resolve(directory);
23965
- if (path9.isAbsolute(trimmed)) {
23966
- candidates.add(trimmed);
23967
- } else {
23968
- candidates.add(path9.resolve(directory, trimmed));
23969
- candidates.add(path9.join(outputDir, trimmed));
23970
- if (!trimmed.endsWith(".md")) {
23971
- candidates.add(path9.join(outputDir, `${trimmed}.md`));
23972
- }
23973
- }
23974
- for (const candidate of candidates) {
23975
- if (path9.extname(candidate) !== ".md") {
23976
- continue;
23977
- }
23978
- const resolved = path9.resolve(candidate);
23979
- if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
23980
- continue;
23981
- }
23982
- if (fsSync.existsSync(candidate)) {
23983
- return candidate;
23984
- }
23985
- }
23986
- return null;
23706
+ return null;
23987
23707
  }
23988
23708
  function slugify(value) {
23989
23709
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
@@ -24104,8 +23824,8 @@ A: ${answer.answer.trim()}` : null;
24104
23824
  }), "utf8");
24105
23825
  }
24106
23826
 
24107
- // src/hooks/session-goal/index.ts
24108
- var COMMAND_NAME = "goal";
23827
+ // src/hooks/goal/index.ts
23828
+ var COMMAND_NAME2 = "goal";
24109
23829
  var MAX_GOAL_LENGTH = 4000;
24110
23830
  function normalizeGoalText(text) {
24111
23831
  return text.trim().replace(/\s+/g, " ").slice(0, MAX_GOAL_LENGTH);
@@ -24162,23 +23882,23 @@ function resolveGoal(goals, sessionID) {
24162
23882
  currentSessionID = goal.inheritedFrom;
24163
23883
  }
24164
23884
  }
24165
- function createSessionGoalHook(ctx, config, options) {
23885
+ function createGoalHook(ctx, config, options) {
24166
23886
  const goals = new Map;
24167
23887
  const outputFolder = config.interview?.outputFolder ?? "interview";
24168
23888
  return {
24169
23889
  registerCommand: (opencodeConfig) => {
24170
23890
  const commandConfig = opencodeConfig.command;
24171
- if (commandConfig?.[COMMAND_NAME])
23891
+ if (commandConfig?.[COMMAND_NAME2])
24172
23892
  return;
24173
23893
  if (!opencodeConfig.command)
24174
23894
  opencodeConfig.command = {};
24175
- opencodeConfig.command[COMMAND_NAME] = {
24176
- template: "Set or show the current session goal",
23895
+ opencodeConfig.command[COMMAND_NAME2] = {
23896
+ template: "Set or show the current goal",
24177
23897
  description: "Pin a session objective that keeps todos, delegation, and verification aligned"
24178
23898
  };
24179
23899
  },
24180
23900
  handleCommandExecuteBefore: async (input, output) => {
24181
- if (input.command !== COMMAND_NAME)
23901
+ if (input.command !== COMMAND_NAME2)
24182
23902
  return;
24183
23903
  output.parts.length = 0;
24184
23904
  const args = input.arguments.trim();
@@ -24190,76 +23910,388 @@ ${resolved.goal.text}
24190
23910
  Use todos for execution steps. Auto-continuation continues only while todos remain.` : "No active goal. Set one with /goal <objective>.");
24191
23911
  return;
24192
23912
  }
24193
- if (args === "clear") {
24194
- goals.delete(input.sessionID);
24195
- pushText(output, "Cleared the active goal for this session.");
23913
+ if (args === "clear") {
23914
+ goals.delete(input.sessionID);
23915
+ pushText(output, "Cleared the active goal for this session.");
23916
+ return;
23917
+ }
23918
+ if (args.startsWith("from ")) {
23919
+ const value = args.slice("from ".length).trim();
23920
+ const interviewGoal = await readInterviewGoal(ctx.directory, outputFolder, value);
23921
+ if (!interviewGoal) {
23922
+ pushText(output, `Could not find a readable interview spec for "${value}".`);
23923
+ return;
23924
+ }
23925
+ goals.set(input.sessionID, {
23926
+ text: interviewGoal.text,
23927
+ source: "interview",
23928
+ sourcePath: interviewGoal.sourcePath,
23929
+ createdAt: Date.now()
23930
+ });
23931
+ pushText(output, `Set active goal from interview:
23932
+ ${interviewGoal.text}`);
23933
+ return;
23934
+ }
23935
+ const text = normalizeGoalText(args);
23936
+ goals.set(input.sessionID, {
23937
+ text,
23938
+ source: "manual",
23939
+ createdAt: Date.now()
23940
+ });
23941
+ pushText(output, `Set active goal:
23942
+ ${text}`);
23943
+ },
23944
+ handleEvent: (input) => {
23945
+ const event = input.event;
23946
+ if (event.type === "session.created") {
23947
+ const info = event.properties?.info;
23948
+ if (!info?.id || !info.parentID)
23949
+ return;
23950
+ const parentGoal = goals.get(info.parentID);
23951
+ if (!parentGoal)
23952
+ return;
23953
+ goals.set(info.id, {
23954
+ inheritedFrom: info.parentID,
23955
+ createdAt: Date.now(),
23956
+ text: ""
23957
+ });
23958
+ return;
23959
+ }
23960
+ if (event.type === "session.deleted") {
23961
+ const props = event.properties;
23962
+ const sessionID = props?.info?.id ?? props?.sessionID;
23963
+ if (sessionID)
23964
+ goals.delete(sessionID);
23965
+ }
23966
+ },
23967
+ handleSystemTransform: (input, output) => {
23968
+ if (!input.sessionID)
23969
+ return;
23970
+ const resolved = resolveGoal(goals, input.sessionID);
23971
+ if (!resolved)
23972
+ return;
23973
+ const agentName = options?.getAgentName?.(input.sessionID);
23974
+ const { goal, inherited } = resolved;
23975
+ if (!inherited && agentName && agentName !== "orchestrator")
23976
+ return;
23977
+ const block = formatGoal(goal, inherited);
23978
+ if (output.system.some((entry) => entry.includes(block)))
23979
+ return;
23980
+ output.system.push(block);
23981
+ },
23982
+ getGoal: (sessionID) => resolveGoal(goals, sessionID)?.goal
23983
+ };
23984
+ }
23985
+ // src/hooks/image-hook.ts
23986
+ import { createHash } from "node:crypto";
23987
+ import {
23988
+ existsSync as existsSync6,
23989
+ mkdirSync as mkdirSync3,
23990
+ readdirSync as readdirSync2,
23991
+ rmdirSync,
23992
+ statSync as statSync3,
23993
+ unlinkSync as unlinkSync2,
23994
+ writeFileSync as writeFileSync3
23995
+ } from "node:fs";
23996
+ import { basename as basename3, extname as extname2, join as join8 } from "node:path";
23997
+ var lastCleanupByDir = new Map;
23998
+ var CLEANUP_INTERVAL = 10 * 60 * 1000;
23999
+ function isImagePart(p) {
24000
+ if (p.type === "image")
24001
+ return true;
24002
+ if (p.type === "file") {
24003
+ const mime = p.mime;
24004
+ if (mime?.startsWith("image/"))
24005
+ return true;
24006
+ const filename = p.filename;
24007
+ const name = p.name;
24008
+ const fileName = filename ?? name;
24009
+ if (fileName && /\.(png|jpg|jpeg|gif|bmp|webp|svg|ico|tiff?|heic)$/i.test(fileName))
24010
+ return true;
24011
+ }
24012
+ return false;
24013
+ }
24014
+ function decodeDataUrl(url) {
24015
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
24016
+ if (!match)
24017
+ return null;
24018
+ return { mime: match[1], data: Buffer.from(match[2], "base64") };
24019
+ }
24020
+ function extFromMime(mime) {
24021
+ const map = {
24022
+ "image/png": ".png",
24023
+ "image/jpeg": ".jpg",
24024
+ "image/gif": ".gif",
24025
+ "image/webp": ".webp",
24026
+ "image/svg+xml": ".svg",
24027
+ "image/bmp": ".bmp"
24028
+ };
24029
+ return map[mime] ?? ".png";
24030
+ }
24031
+ function sanitizeFilename(name) {
24032
+ return name.replace(/[^a-zA-Z0-9._-]/g, "_");
24033
+ }
24034
+ function cleanupAllSessions(saveDir) {
24035
+ const now = Date.now();
24036
+ const lastCleanup = lastCleanupByDir.get(saveDir) ?? 0;
24037
+ if (now - lastCleanup < CLEANUP_INTERVAL)
24038
+ return;
24039
+ lastCleanupByDir.set(saveDir, now);
24040
+ const maxAge = 60 * 60 * 1000;
24041
+ const dirsToScan = [];
24042
+ try {
24043
+ for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
24044
+ const fp = join8(saveDir, entry.name);
24045
+ if (entry.isDirectory()) {
24046
+ dirsToScan.push(fp);
24047
+ } else {
24048
+ try {
24049
+ if (now - statSync3(fp).mtimeMs > maxAge)
24050
+ unlinkSync2(fp);
24051
+ } catch {}
24052
+ }
24053
+ }
24054
+ } catch {}
24055
+ for (const dir of dirsToScan) {
24056
+ try {
24057
+ let isEmpty = true;
24058
+ let allRemoved = true;
24059
+ for (const f of readdirSync2(dir)) {
24060
+ isEmpty = false;
24061
+ const fp = join8(dir, f);
24062
+ try {
24063
+ if (now - statSync3(fp).mtimeMs > maxAge) {
24064
+ unlinkSync2(fp);
24065
+ } else {
24066
+ allRemoved = false;
24067
+ }
24068
+ } catch {
24069
+ allRemoved = false;
24070
+ }
24071
+ }
24072
+ if (!isEmpty && allRemoved) {
24073
+ try {
24074
+ rmdirSync(dir);
24075
+ } catch {}
24076
+ }
24077
+ } catch {}
24078
+ }
24079
+ }
24080
+ function writeUniqueFile(dir, name, data, log2) {
24081
+ const ext = extname2(name);
24082
+ const base = basename3(name, ext) || name;
24083
+ let candidate = join8(dir, name);
24084
+ if (existsSync6(candidate)) {
24085
+ return candidate;
24086
+ }
24087
+ let counter = 0;
24088
+ const MAX_ATTEMPTS = 1000;
24089
+ for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
24090
+ try {
24091
+ writeFileSync3(candidate, data, { flag: "wx" });
24092
+ return candidate;
24093
+ } catch (e) {
24094
+ if (e instanceof Error && e.code === "EEXIST") {
24095
+ counter += 1;
24096
+ candidate = join8(dir, `${base}-${counter}${ext}`);
24097
+ continue;
24098
+ }
24099
+ log2(`[image-hook] failed to save image: ${e}`);
24100
+ return null;
24101
+ }
24102
+ }
24103
+ log2(`[image-hook] failed to save image: max attempts (${MAX_ATTEMPTS}) reached`);
24104
+ return null;
24105
+ }
24106
+ function processImageAttachments(args) {
24107
+ const { messages, workDir, disabledAgents, log: log2 } = args;
24108
+ const observerEnabled = !disabledAgents.has("observer");
24109
+ if (!observerEnabled)
24110
+ return;
24111
+ const messagesWithImages = [];
24112
+ for (const msg of messages) {
24113
+ if (msg.info.role !== "user")
24114
+ continue;
24115
+ const imageParts = msg.parts.filter(isImagePart);
24116
+ if (imageParts.length > 0) {
24117
+ messagesWithImages.push({ msg, imageParts });
24118
+ }
24119
+ }
24120
+ const saveDir = join8(workDir, ".opencode", "images");
24121
+ if (messagesWithImages.length === 0) {
24122
+ if (existsSync6(saveDir))
24123
+ cleanupAllSessions(saveDir);
24124
+ return;
24125
+ }
24126
+ const gitignorePath = join8(workDir, ".opencode", ".gitignore");
24127
+ try {
24128
+ mkdirSync3(saveDir, { recursive: true });
24129
+ if (!existsSync6(gitignorePath))
24130
+ writeFileSync3(gitignorePath, `*
24131
+ `);
24132
+ } catch (e) {
24133
+ log2(`[image-hook] failed to create image directory: ${e}`);
24134
+ }
24135
+ cleanupAllSessions(saveDir);
24136
+ for (const { msg, imageParts } of messagesWithImages) {
24137
+ const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
24138
+ const targetDir = sessionSubdir ? join8(saveDir, sessionSubdir) : saveDir;
24139
+ try {
24140
+ mkdirSync3(targetDir, { recursive: true });
24141
+ } catch (e) {
24142
+ log2(`[image-hook] failed to create target image directory: ${e}`);
24143
+ }
24144
+ const savedPaths = [];
24145
+ for (const p of imageParts) {
24146
+ const url = p.url;
24147
+ const filename = p.filename ?? p.name;
24148
+ if (url) {
24149
+ const decoded = decodeDataUrl(url);
24150
+ if (decoded) {
24151
+ const hash = createHash("sha1").update(decoded.data).digest("hex").slice(0, 8);
24152
+ const sanitizedFilename = filename ? sanitizeFilename(filename) : undefined;
24153
+ const baseName = sanitizedFilename ? sanitizedFilename.replace(/\.[^.]+$/, "") || "image" : "image";
24154
+ const ext = sanitizedFilename ? extname2(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
24155
+ const name = `${baseName}-${hash}${ext}`;
24156
+ const filePath = writeUniqueFile(targetDir, name, decoded.data, log2);
24157
+ if (filePath)
24158
+ savedPaths.push(filePath);
24159
+ }
24160
+ }
24161
+ }
24162
+ const pathsText = savedPaths.length > 0 ? ` Saved to: ${savedPaths.join(", ")}` : "";
24163
+ log2(`[image-hook] stripping image/file parts, saving to disk${pathsText}`);
24164
+ msg.parts = msg.parts.filter((p) => !isImagePart(p)).concat([
24165
+ {
24166
+ type: "text",
24167
+ text: `[Image attachment detected.${pathsText} Your model may not support image input. Delegate to @observer with the file path(s) above so it can read the file with its read tool.]`
24168
+ }
24169
+ ]);
24170
+ }
24171
+ }
24172
+ // src/hooks/json-error-recovery/hook.ts
24173
+ var JSON_ERROR_TOOL_EXCLUDE_LIST = [
24174
+ "bash",
24175
+ "read",
24176
+ "glob",
24177
+ "webfetch",
24178
+ "grep_app_searchgithub",
24179
+ "websearch_web_search_exa"
24180
+ ];
24181
+ var JSON_ERROR_PATTERNS = [
24182
+ /json parse error/i,
24183
+ /failed to parse json/i,
24184
+ /invalid json/i,
24185
+ /malformed json/i,
24186
+ /unexpected end of json input/i,
24187
+ /syntaxerror:\s*unexpected token.*json/i,
24188
+ /json[^\n]*expected '\}'/i,
24189
+ /json[^\n]*unexpected eof/i
24190
+ ];
24191
+ var JSON_ERROR_REMINDER_MARKER = "[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]";
24192
+ var JSON_ERROR_EXCLUDED_TOOLS = new Set(JSON_ERROR_TOOL_EXCLUDE_LIST);
24193
+ var JSON_ERROR_REMINDER = `
24194
+ [JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
24195
+
24196
+ You sent invalid JSON arguments. The system could not parse your tool call.
24197
+ STOP and do this NOW:
24198
+
24199
+ 1. LOOK at the error message above to see what was expected vs what you sent.
24200
+ 2. CORRECT your JSON syntax (missing braces, unescaped quotes, trailing commas, etc).
24201
+ 3. RETRY the tool call with valid JSON.
24202
+
24203
+ DO NOT repeat the exact same invalid call.
24204
+ `;
24205
+ function createJsonErrorRecoveryHook(_ctx) {
24206
+ return {
24207
+ "tool.execute.after": async (input, output) => {
24208
+ if (JSON_ERROR_EXCLUDED_TOOLS.has(input.tool.toLowerCase()))
24209
+ return;
24210
+ if (typeof output.output !== "string")
24211
+ return;
24212
+ if (output.output.includes(JSON_ERROR_REMINDER_MARKER))
24213
+ return;
24214
+ const outputText = output.output;
24215
+ const hasJsonError = JSON_ERROR_PATTERNS.some((pattern) => pattern.test(outputText));
24216
+ if (hasJsonError) {
24217
+ output.output += `
24218
+ ${JSON_ERROR_REMINDER}`;
24219
+ }
24220
+ }
24221
+ };
24222
+ }
24223
+ // src/hooks/phase-reminder/index.ts
24224
+ var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
24225
+ function createPhaseReminderHook() {
24226
+ return {
24227
+ "experimental.chat.messages.transform": async (_input, output) => {
24228
+ const { messages } = output;
24229
+ if (messages.length === 0) {
24230
+ return;
24231
+ }
24232
+ let lastUserMessageIndex = -1;
24233
+ for (let i = messages.length - 1;i >= 0; i--) {
24234
+ if (messages[i].info.role === "user") {
24235
+ lastUserMessageIndex = i;
24236
+ break;
24237
+ }
24238
+ }
24239
+ if (lastUserMessageIndex === -1) {
24240
+ return;
24241
+ }
24242
+ const lastUserMessage = messages[lastUserMessageIndex];
24243
+ const agent = lastUserMessage.info.agent;
24244
+ if (agent && agent !== "orchestrator") {
24196
24245
  return;
24197
24246
  }
24198
- if (args.startsWith("from ")) {
24199
- const value = args.slice("from ".length).trim();
24200
- const interviewGoal = await readInterviewGoal(ctx.directory, outputFolder, value);
24201
- if (!interviewGoal) {
24202
- pushText(output, `Could not find a readable interview spec for "${value}".`);
24203
- return;
24204
- }
24205
- goals.set(input.sessionID, {
24206
- text: interviewGoal.text,
24207
- source: "interview",
24208
- sourcePath: interviewGoal.sourcePath,
24209
- createdAt: Date.now()
24210
- });
24211
- pushText(output, `Set active goal from interview:
24212
- ${interviewGoal.text}`);
24247
+ const textPartIndex = lastUserMessage.parts.findIndex((p) => p.type === "text" && p.text !== undefined);
24248
+ if (textPartIndex === -1) {
24213
24249
  return;
24214
24250
  }
24215
- const text = normalizeGoalText(args);
24216
- goals.set(input.sessionID, {
24217
- text,
24218
- source: "manual",
24219
- createdAt: Date.now()
24220
- });
24221
- pushText(output, `Set active goal:
24222
- ${text}`);
24223
- },
24224
- handleEvent: (input) => {
24225
- const event = input.event;
24226
- if (event.type === "session.created") {
24227
- const info = event.properties?.info;
24228
- if (!info?.id || !info.parentID)
24229
- return;
24230
- const parentGoal = goals.get(info.parentID);
24231
- if (!parentGoal)
24232
- return;
24233
- goals.set(info.id, {
24234
- inheritedFrom: info.parentID,
24235
- createdAt: Date.now(),
24236
- text: ""
24237
- });
24251
+ const originalText = lastUserMessage.parts[textPartIndex].text ?? "";
24252
+ if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
24238
24253
  return;
24239
24254
  }
24240
- if (event.type === "session.deleted") {
24241
- const props = event.properties;
24242
- const sessionID = props?.info?.id ?? props?.sessionID;
24243
- if (sessionID)
24244
- goals.delete(sessionID);
24245
- }
24246
- },
24247
- handleSystemTransform: (input, output) => {
24248
- if (!input.sessionID)
24249
- return;
24250
- const resolved = resolveGoal(goals, input.sessionID);
24251
- if (!resolved)
24255
+ if (lastUserMessage.parts.some((p) => p.text?.includes(PHASE_REMINDER))) {
24252
24256
  return;
24253
- const agentName = options?.getAgentName?.(input.sessionID);
24254
- const { goal, inherited } = resolved;
24255
- if (!inherited && agentName && agentName !== "orchestrator")
24257
+ }
24258
+ lastUserMessage.parts.push({
24259
+ type: "text",
24260
+ text: PHASE_REMINDER
24261
+ });
24262
+ }
24263
+ };
24264
+ }
24265
+ // src/hooks/post-file-tool-nudge/index.ts
24266
+ var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
24267
+ var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
24268
+ function createPostFileToolNudgeHook(options = {}) {
24269
+ function appendReminder(output) {
24270
+ if (typeof output.output !== "string") {
24271
+ return;
24272
+ }
24273
+ if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
24274
+ return;
24275
+ }
24276
+ output.output = [
24277
+ output.output,
24278
+ "",
24279
+ "<internal_reminder>",
24280
+ POST_FILE_TOOL_NUDGE,
24281
+ "</internal_reminder>"
24282
+ ].join(`
24283
+ `);
24284
+ }
24285
+ return {
24286
+ "tool.execute.after": async (input, output) => {
24287
+ if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
24256
24288
  return;
24257
- const block = formatGoal(goal, inherited);
24258
- if (output.system.some((entry) => entry.includes(block)))
24289
+ }
24290
+ if (options.shouldInject && !options.shouldInject(input.sessionID)) {
24259
24291
  return;
24260
- output.system.push(block);
24261
- },
24262
- getGoal: (sessionID) => resolveGoal(goals, sessionID)?.goal
24292
+ }
24293
+ appendReminder(output);
24294
+ }
24263
24295
  };
24264
24296
  }
24265
24297
  // src/hooks/task-session-manager/index.ts
@@ -24842,7 +24874,7 @@ function createTodoHygiene(options) {
24842
24874
 
24843
24875
  // src/hooks/todo-continuation/index.ts
24844
24876
  var HOOK_NAME = "todo-continuation";
24845
- var COMMAND_NAME2 = "auto-continue";
24877
+ var COMMAND_NAME3 = "auto-continue";
24846
24878
  var TODO_STATE_TIMEOUT_MS = 500;
24847
24879
  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.]";
24848
24880
  var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
@@ -25360,7 +25392,7 @@ function createTodoContinuationHook(ctx, config) {
25360
25392
  }
25361
25393
  }
25362
25394
  async function handleCommandExecuteBefore(input, output) {
25363
- if (input.command !== COMMAND_NAME2) {
25395
+ if (input.command !== COMMAND_NAME3) {
25364
25396
  return;
25365
25397
  }
25366
25398
  registerOrchestratorSession(input.sessionID);
@@ -25379,11 +25411,11 @@ function createTodoContinuationHook(ctx, config) {
25379
25411
  if (!newEnabled) {
25380
25412
  cancelPendingTimer(state);
25381
25413
  output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
25382
- log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME2} command`);
25414
+ log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME3} command`);
25383
25415
  return;
25384
25416
  }
25385
25417
  state.suppressUntil = 0;
25386
- log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME2} command`, {
25418
+ log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME3} command`, {
25387
25419
  maxContinuations
25388
25420
  });
25389
25421
  let hasIncompleteTodos = false;
@@ -28229,7 +28261,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
28229
28261
  }
28230
28262
 
28231
28263
  // src/interview/service.ts
28232
- var COMMAND_NAME3 = "interview";
28264
+ var COMMAND_NAME4 = "interview";
28233
28265
  var DEFAULT_MAX_QUESTIONS = 2;
28234
28266
  function isTruthyEnvFlag(value) {
28235
28267
  if (!value) {
@@ -28476,11 +28508,11 @@ function createInterviewService(ctx, config, deps) {
28476
28508
  }
28477
28509
  function registerCommand(opencodeConfig) {
28478
28510
  const configCommand = opencodeConfig.command;
28479
- if (!configCommand?.[COMMAND_NAME3]) {
28511
+ if (!configCommand?.[COMMAND_NAME4]) {
28480
28512
  if (!opencodeConfig.command) {
28481
28513
  opencodeConfig.command = {};
28482
28514
  }
28483
- opencodeConfig.command[COMMAND_NAME3] = {
28515
+ opencodeConfig.command[COMMAND_NAME4] = {
28484
28516
  template: "Start an interview and write a live markdown spec",
28485
28517
  description: "Open a localhost interview UI linked to the current OpenCode session"
28486
28518
  };
@@ -28554,7 +28586,7 @@ function createInterviewService(ctx, config, deps) {
28554
28586
  }
28555
28587
  }
28556
28588
  async function handleCommandExecuteBefore(input, output) {
28557
- if (input.command !== COMMAND_NAME3) {
28589
+ if (input.command !== COMMAND_NAME4) {
28558
28590
  return;
28559
28591
  }
28560
28592
  const idea = input.arguments.trim();
@@ -30808,11 +30840,11 @@ function recordTuiAgentModel(input) {
30808
30840
  }
30809
30841
 
30810
30842
  // src/tools/preset-manager.ts
30811
- var COMMAND_NAME4 = "preset";
30843
+ var COMMAND_NAME5 = "preset";
30812
30844
  function createPresetManager(ctx, config) {
30813
30845
  let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
30814
30846
  async function handleCommandExecuteBefore(input, output) {
30815
- if (input.command !== COMMAND_NAME4) {
30847
+ if (input.command !== COMMAND_NAME5) {
30816
30848
  return;
30817
30849
  }
30818
30850
  output.parts.length = 0;
@@ -30831,11 +30863,11 @@ function createPresetManager(ctx, config) {
30831
30863
  }
30832
30864
  function registerCommand(opencodeConfig) {
30833
30865
  const configCommand = opencodeConfig.command;
30834
- if (!configCommand?.[COMMAND_NAME4]) {
30866
+ if (!configCommand?.[COMMAND_NAME5]) {
30835
30867
  if (!opencodeConfig.command) {
30836
30868
  opencodeConfig.command = {};
30837
30869
  }
30838
- opencodeConfig.command[COMMAND_NAME4] = {
30870
+ opencodeConfig.command[COMMAND_NAME5] = {
30839
30871
  template: "List available presets and switch between them",
30840
30872
  description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
30841
30873
  };
@@ -33269,460 +33301,6 @@ function createWebfetchTool(pluginCtx, options = {}) {
33269
33301
  }
33270
33302
  });
33271
33303
  }
33272
- // src/tools/subtask/command.ts
33273
- var COMMAND_NAME5 = "subtask";
33274
- var SUBTASK_COMMAND_TEMPLATE = `Start a focused subtask worker.
33275
-
33276
- The user's request below is the full scope for the worker. Do not broaden it.
33277
- Create a self-contained worker prompt that includes:
33278
- - the exact objective
33279
- - relevant context from this conversation
33280
- - specific files/paths that matter
33281
- - expected deliverables
33282
- - validation the worker should run, if applicable
33283
-
33284
- USER REQUEST:
33285
- $ARGUMENTS
33286
-
33287
- Then call the subtask tool:
33288
- \`subtask(prompt="...", files=["src/foo.ts", "docs/bar.md"])\`
33289
-
33290
- Only include files that are clearly relevant. If no files are needed, omit files.`;
33291
- function createSubtaskCommandManager(_ctx, state) {
33292
- function registerCommand(opencodeConfig) {
33293
- const configCommand = opencodeConfig.command;
33294
- if (!configCommand?.[COMMAND_NAME5]) {
33295
- if (!opencodeConfig.command) {
33296
- opencodeConfig.command = {};
33297
- }
33298
- opencodeConfig.command[COMMAND_NAME5] = {
33299
- description: "Create a focused subtask prompt for a new session",
33300
- template: SUBTASK_COMMAND_TEMPLATE
33301
- };
33302
- }
33303
- }
33304
- return {
33305
- registerCommand,
33306
- handleEvent(input) {
33307
- if (input.event.type === "session.created") {
33308
- const info = input.event.properties?.info;
33309
- if (!info?.id || !info.parentID)
33310
- return;
33311
- const source = state.sourceFor(info.parentID);
33312
- if (source)
33313
- state.markSession(info.id, source);
33314
- return;
33315
- }
33316
- if (input.event.type !== "session.deleted")
33317
- return;
33318
- const sessionID = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
33319
- if (sessionID)
33320
- state.unmarkSession(sessionID);
33321
- }
33322
- };
33323
- }
33324
- // src/tools/subtask/files.ts
33325
- import * as fs12 from "node:fs/promises";
33326
- import * as path20 from "node:path";
33327
-
33328
- // src/tools/subtask/vendor.ts
33329
- import * as fs11 from "node:fs/promises";
33330
- import * as path19 from "node:path";
33331
- var DEFAULT_READ_LIMIT = 2000;
33332
- var MAX_LINE_LENGTH = 2000;
33333
- var MAX_BYTES = 50 * 1024;
33334
- var MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
33335
- var MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`;
33336
- var SAMPLE_BYTES = 4096;
33337
- var BINARY_EXTENSIONS = new Set([
33338
- ".zip",
33339
- ".tar",
33340
- ".gz",
33341
- ".exe",
33342
- ".dll",
33343
- ".so",
33344
- ".class",
33345
- ".jar",
33346
- ".war",
33347
- ".7z",
33348
- ".doc",
33349
- ".docx",
33350
- ".xls",
33351
- ".xlsx",
33352
- ".ppt",
33353
- ".pptx",
33354
- ".odt",
33355
- ".ods",
33356
- ".odp",
33357
- ".bin",
33358
- ".dat",
33359
- ".obj",
33360
- ".o",
33361
- ".a",
33362
- ".lib",
33363
- ".wasm",
33364
- ".pyc",
33365
- ".pyo"
33366
- ]);
33367
- async function isBinaryFile(filepath) {
33368
- const ext = path19.extname(filepath).toLowerCase();
33369
- if (BINARY_EXTENSIONS.has(ext)) {
33370
- return true;
33371
- }
33372
- try {
33373
- const file = await fs11.open(filepath, "r");
33374
- try {
33375
- const buffer = Buffer.alloc(SAMPLE_BYTES);
33376
- const result = await file.read(buffer, 0, SAMPLE_BYTES, 0);
33377
- if (result.bytesRead === 0)
33378
- return false;
33379
- const bytes = buffer.subarray(0, result.bytesRead);
33380
- let nonPrintableCount = 0;
33381
- for (let i = 0;i < bytes.length; i++) {
33382
- const byte = bytes[i];
33383
- if (byte === undefined)
33384
- continue;
33385
- if (byte === 0)
33386
- return true;
33387
- if (byte < 9 || byte > 13 && byte < 32) {
33388
- nonPrintableCount++;
33389
- }
33390
- }
33391
- return nonPrintableCount / bytes.length > 0.3;
33392
- } finally {
33393
- await file.close();
33394
- }
33395
- } catch {
33396
- return false;
33397
- }
33398
- }
33399
- function formatFileContent(_filepath, content) {
33400
- const cappedContent = Buffer.byteLength(content, "utf8") > MAX_BYTES;
33401
- const contentToFormat = cappedContent ? content.slice(0, MAX_BYTES) : content;
33402
- const lines = contentToFormat.split(`
33403
- `);
33404
- const limit = DEFAULT_READ_LIMIT;
33405
- const offset = 0;
33406
- const raw = lines.slice(offset, offset + limit).map((line) => {
33407
- return line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}${MAX_LINE_SUFFIX}` : line;
33408
- });
33409
- const formatted = raw.map((line, index) => {
33410
- return `${index + offset + 1}: ${line}`;
33411
- });
33412
- let output = [
33413
- `<path>${_filepath}</path>`,
33414
- "<type>file</type>",
33415
- `<content>
33416
- `
33417
- ].join(`
33418
- `);
33419
- output += formatted.join(`
33420
- `);
33421
- const totalLines = lines.length;
33422
- const lastReadLine = offset + formatted.length;
33423
- const hasMoreLines = totalLines > lastReadLine;
33424
- if (cappedContent) {
33425
- output += `
33426
-
33427
- (Output capped at ${MAX_BYTES_LABEL}. Showing lines 1-${lastReadLine}. Use offset=${lastReadLine + 1} to continue.)`;
33428
- } else if (hasMoreLines) {
33429
- output += `
33430
-
33431
- (Showing lines 1-${lastReadLine} of ${totalLines}. Use offset=${lastReadLine + 1} to continue.)`;
33432
- } else {
33433
- output += `
33434
-
33435
- (End of file - total ${totalLines} lines)`;
33436
- }
33437
- output += `
33438
- </content>`;
33439
- return output;
33440
- }
33441
-
33442
- // src/tools/subtask/files.ts
33443
- var FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
33444
- var TRAILING_PATH_PUNCTUATION = /[!?:;]+$/;
33445
- function cleanFileReference(ref) {
33446
- return ref.replace(/^@/, "").replace(TRAILING_PATH_PUNCTUATION, "");
33447
- }
33448
- function parseFileReferences(text) {
33449
- const fileRefs = new Set;
33450
- for (const match of text.matchAll(FILE_REGEX)) {
33451
- if (match[1]) {
33452
- fileRefs.add(cleanFileReference(match[1]));
33453
- }
33454
- }
33455
- return fileRefs;
33456
- }
33457
- async function buildSyntheticFileParts(directory, refs) {
33458
- const parts = [];
33459
- const realDirectory = await fs12.realpath(directory);
33460
- for (const ref of refs) {
33461
- const filepath = path20.resolve(directory, ref);
33462
- const relative3 = path20.relative(directory, filepath);
33463
- if (relative3.startsWith("..") || path20.isAbsolute(relative3))
33464
- continue;
33465
- try {
33466
- const realFilepath = await fs12.realpath(filepath);
33467
- const realRelative = path20.relative(realDirectory, realFilepath);
33468
- if (realRelative.startsWith("..") || path20.isAbsolute(realRelative)) {
33469
- continue;
33470
- }
33471
- const stats = await fs12.stat(realFilepath);
33472
- if (!stats.isFile())
33473
- continue;
33474
- if (await isBinaryFile(realFilepath))
33475
- continue;
33476
- const content = await fs12.readFile(realFilepath, "utf-8");
33477
- parts.push({
33478
- type: "text",
33479
- synthetic: true,
33480
- text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: realFilepath })}`
33481
- });
33482
- parts.push({
33483
- type: "text",
33484
- synthetic: true,
33485
- text: formatFileContent(realFilepath, content)
33486
- });
33487
- } catch {}
33488
- }
33489
- return parts;
33490
- }
33491
- // src/tools/subtask/state.ts
33492
- function createSubtaskState() {
33493
- const sourceBySession = new Map;
33494
- return {
33495
- markSession(sessionID, sourceSessionID) {
33496
- sourceBySession.set(sessionID, sourceSessionID);
33497
- },
33498
- unmarkSession(sessionID) {
33499
- sourceBySession.delete(sessionID);
33500
- },
33501
- isSubtaskSession(sessionID) {
33502
- return sourceBySession.has(sessionID);
33503
- },
33504
- sourceFor(sessionID) {
33505
- return sourceBySession.get(sessionID);
33506
- }
33507
- };
33508
- }
33509
- // src/tools/subtask/tools.ts
33510
- import { tool as tool5 } from "@opencode-ai/plugin";
33511
- var SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
33512
- var SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
33513
- function normalizeSubtaskSummary(text) {
33514
- return text.replace(SUBTASK_SUMMARY_TAG_REGEX, "").trim();
33515
- }
33516
- function getAbortSignal(context) {
33517
- if (!context || typeof context !== "object" || !("abort" in context)) {
33518
- return;
33519
- }
33520
- const signal = context.abort;
33521
- return signal && typeof signal === "object" && "addEventListener" in signal && "removeEventListener" in signal && "aborted" in signal ? signal : undefined;
33522
- }
33523
- function createSubtaskTool(ctx, state, depthTracker) {
33524
- const client = ctx.client;
33525
- return tool5({
33526
- description: "Run a child worker session and return its completion summary to the caller",
33527
- args: {
33528
- prompt: tool5.schema.string().describe("The generated subtask prompt"),
33529
- files: tool5.schema.array(tool5.schema.string()).optional().describe("Array of file paths to load into the new session's context")
33530
- },
33531
- async execute(args, context) {
33532
- const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : ctx.directory;
33533
- const sessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : "unknown";
33534
- const abortSignal = getAbortSignal(context);
33535
- if (state.isSubtaskSession(sessionID)) {
33536
- return "Nested subtask is disabled: this session is already a subtask worker. Finish this worker and return its summary to the parent session instead.";
33537
- }
33538
- if (sessionID !== "unknown" && depthTracker && depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth) {
33539
- return `Subtask worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
33540
- }
33541
- const sessionReference = `You are a subtask worker spawned by parent session ${sessionID}.
33542
-
33543
- Your job is bounded: complete only the task below. Do not expand scope.
33544
- If needed context is missing, use read_session to inspect the parent session.
33545
- Do not spawn another subtask.`;
33546
- const files = new Set([
33547
- ...parseFileReferences(args.prompt),
33548
- ...(args.files ?? []).map(cleanFileReference)
33549
- ]);
33550
- const fileRefs = files.size > 0 ? [...files].map((f) => `@${f}`).join(" ") : "";
33551
- const fullPrompt = fileRefs ? `${sessionReference}
33552
-
33553
- TASK:
33554
- ${args.prompt}
33555
-
33556
- FILES PROVIDED:
33557
- ${fileRefs}` : `${sessionReference}
33558
-
33559
- TASK:
33560
- ${args.prompt}`;
33561
- let childSessionID;
33562
- try {
33563
- const session2 = await client.session.create({
33564
- responseStyle: "data",
33565
- throwOnError: true,
33566
- query: { directory },
33567
- body: {
33568
- parentID: sessionID === "unknown" ? undefined : sessionID,
33569
- title: `Subtask worker from ${sessionID}`
33570
- }
33571
- });
33572
- childSessionID = session2?.data?.id ?? session2?.id;
33573
- if (!childSessionID) {
33574
- throw new Error("Subtask worker session did not return an id");
33575
- }
33576
- if (sessionID !== "unknown" && depthTracker) {
33577
- const registered = depthTracker.registerChild(sessionID, childSessionID);
33578
- if (!registered) {
33579
- throw new Error("Subtask worker blocked: max subagent depth exceeded");
33580
- }
33581
- }
33582
- state.markSession(childSessionID, sessionID);
33583
- await promptWithTimeout(client, {
33584
- responseStyle: "data",
33585
- throwOnError: true,
33586
- query: { directory },
33587
- path: { id: childSessionID },
33588
- body: {
33589
- agent: "orchestrator",
33590
- parts: [
33591
- {
33592
- type: "text",
33593
- text: `${fullPrompt}
33594
-
33595
- Instructions:
33596
- 1. Understand the task and relevant file context.
33597
- 2. Make only necessary changes.
33598
- 3. Run the most relevant validation checks when practical.
33599
- 4. Stop when the requested task is done.
33600
-
33601
- Return your final response in this format:
33602
-
33603
- <subtask_summary>
33604
- Status: completed | blocked | partial
33605
-
33606
- What changed:
33607
- - ...
33608
-
33609
- Files touched:
33610
- - ...
33611
-
33612
- Validation:
33613
- - ...
33614
-
33615
- Risks / follow-up:
33616
- - ...
33617
- </subtask_summary>`
33618
- },
33619
- ...await buildSyntheticFileParts(directory, files)
33620
- ]
33621
- }
33622
- }, SUBTASK_TIMEOUT_MS, abortSignal);
33623
- const extraction = await extractSessionResult(client, childSessionID, {
33624
- directory,
33625
- includeReasoning: false
33626
- });
33627
- if (extraction.empty) {
33628
- throw new Error("Subtask worker returned no summary");
33629
- }
33630
- const summary = normalizeSubtaskSummary(extraction.text);
33631
- return [
33632
- `task_id: ${childSessionID}`,
33633
- "",
33634
- "<subtask_summary>",
33635
- summary,
33636
- "</subtask_summary>"
33637
- ].join(`
33638
- `);
33639
- } finally {
33640
- if (childSessionID) {
33641
- try {
33642
- await client.session.abort({
33643
- path: { id: childSessionID },
33644
- query: { directory }
33645
- });
33646
- state.unmarkSession(childSessionID);
33647
- } catch {}
33648
- }
33649
- }
33650
- }
33651
- });
33652
- }
33653
- function formatTranscript(messages, limit) {
33654
- const lines = [];
33655
- for (const msg of messages) {
33656
- const role = msg.info?.role;
33657
- const parts = msg.parts;
33658
- if (role === "user") {
33659
- lines.push("## User");
33660
- for (const part of parts) {
33661
- if (part.type === "text" && !part.ignored && typeof part.text === "string") {
33662
- lines.push(part.text);
33663
- }
33664
- if (part.type === "file") {
33665
- lines.push(`[Attached: ${part.filename || "file"}]`);
33666
- }
33667
- }
33668
- lines.push("");
33669
- }
33670
- if (role === "assistant") {
33671
- lines.push("## Assistant");
33672
- for (const part of parts) {
33673
- if (part.type === "text" && typeof part.text === "string") {
33674
- lines.push(part.text);
33675
- }
33676
- if (part.type === "tool" && part.state?.status === "completed" && part.tool) {
33677
- lines.push(`[Tool: ${part.tool}] ${part.state.title ?? ""}`);
33678
- }
33679
- }
33680
- lines.push("");
33681
- }
33682
- }
33683
- const output = lines.join(`
33684
- `).trim();
33685
- if (messages.length >= (limit ?? 100)) {
33686
- return output + `
33687
-
33688
- (Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`;
33689
- }
33690
- return `${output}
33691
-
33692
- (End of session - ${messages.length} messages)`;
33693
- }
33694
- function createReadSessionTool(client, state) {
33695
- return tool5({
33696
- description: "Read the conversation transcript from a previous session. Use this when you need specific information from the source session that wasn't included in the subtask summary.",
33697
- args: {
33698
- sessionID: tool5.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
33699
- limit: tool5.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)")
33700
- },
33701
- async execute(args, context) {
33702
- const limit = Math.min(args.limit ?? 100, 500);
33703
- const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : undefined;
33704
- const callerSessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : undefined;
33705
- if (!callerSessionID || !state.isSubtaskSession(callerSessionID)) {
33706
- return "read_session is only available from subtask worker sessions.";
33707
- }
33708
- if (state.sourceFor(callerSessionID) !== args.sessionID) {
33709
- return "read_session can only read the source session for this subtask worker.";
33710
- }
33711
- try {
33712
- const response = await client.session.messages({
33713
- path: { id: args.sessionID },
33714
- query: { limit, ...directory ? { directory } : {} }
33715
- });
33716
- if (!response.data || response.data.length === 0) {
33717
- return "Session has no messages or does not exist.";
33718
- }
33719
- return formatTranscript(response.data, limit);
33720
- } catch (error) {
33721
- return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : "Unknown error"}`;
33722
- }
33723
- }
33724
- });
33725
- }
33726
33304
  // src/utils/subagent-depth.ts
33727
33305
  class SubagentDepthTracker {
33728
33306
  depthBySession = new Map;
@@ -33835,7 +33413,8 @@ var OhMyOpenCodeLite = async (ctx) => {
33835
33413
  let jsonErrorRecoveryHook;
33836
33414
  let foregroundFallback;
33837
33415
  let todoContinuationHook;
33838
- let sessionGoalHook;
33416
+ let deepworkCommandHook;
33417
+ let goalHook;
33839
33418
  let taskSessionManagerHook;
33840
33419
  let backgroundJobBoard;
33841
33420
  let interviewManager;
@@ -33844,8 +33423,6 @@ var OhMyOpenCodeLite = async (ctx) => {
33844
33423
  let councilTools;
33845
33424
  let webfetch;
33846
33425
  let rewriteDisplayNameMentions;
33847
- let subtaskCommandManager;
33848
- let subtaskState;
33849
33426
  let toolCount = 0;
33850
33427
  try {
33851
33428
  config = loadPluginConfig(ctx.directory);
@@ -33931,7 +33508,8 @@ var OhMyOpenCodeLite = async (ctx) => {
33931
33508
  autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
33932
33509
  backgroundJobBoard
33933
33510
  });
33934
- sessionGoalHook = createSessionGoalHook(ctx, config, {
33511
+ deepworkCommandHook = createDeepworkCommandHook();
33512
+ goalHook = createGoalHook(ctx, config, {
33935
33513
  getAgentName: (sessionID) => sessionAgentMap.get(sessionID)
33936
33514
  });
33937
33515
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
@@ -33944,9 +33522,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33944
33522
  interviewManager = createInterviewManager(ctx, config);
33945
33523
  presetManager = createPresetManager(ctx, config);
33946
33524
  divoomManager = createDivoomManager(config.divoom);
33947
- subtaskState = createSubtaskState();
33948
- subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
33949
- toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2 + 2;
33525
+ toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
33950
33526
  } catch (err) {
33951
33527
  log("[plugin] FATAL: init failed", String(err));
33952
33528
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -33991,9 +33567,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33991
33567
  webfetch,
33992
33568
  ...todoContinuationHook.tool,
33993
33569
  ast_grep_search,
33994
- ast_grep_replace,
33995
- subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
33996
- read_session: createReadSessionTool(ctx.client, subtaskState)
33570
+ ast_grep_replace
33997
33571
  },
33998
33572
  mcp: mcps,
33999
33573
  config: async (opencodeConfig) => {
@@ -34189,9 +33763,9 @@ var OhMyOpenCodeLite = async (ctx) => {
34189
33763
  };
34190
33764
  }
34191
33765
  interviewManager.registerCommand(opencodeConfig);
34192
- sessionGoalHook.registerCommand(opencodeConfig);
33766
+ goalHook.registerCommand(opencodeConfig);
33767
+ deepworkCommandHook.registerCommand(opencodeConfig);
34193
33768
  presetManager.registerCommand(opencodeConfig);
34194
- subtaskCommandManager.registerCommand(opencodeConfig);
34195
33769
  },
34196
33770
  event: async (input) => {
34197
33771
  const event = input.event;
@@ -34216,11 +33790,10 @@ var OhMyOpenCodeLite = async (ctx) => {
34216
33790
  await multiplexerSessionManager.onSessionDeleted(event);
34217
33791
  await foregroundFallback.handleEvent(input.event);
34218
33792
  await todoContinuationHook.handleEvent(input);
34219
- sessionGoalHook.handleEvent(input);
33793
+ goalHook.handleEvent(input);
34220
33794
  await autoUpdateChecker.event(input);
34221
33795
  await interviewManager.handleEvent(input);
34222
33796
  await taskSessionManagerHook.event(input);
34223
- subtaskCommandManager.handleEvent(input);
34224
33797
  if (event.type === "permission.asked" || event.type === "question.asked") {
34225
33798
  const props = event.properties;
34226
33799
  divoomManager.onUserInputRequired({
@@ -34278,7 +33851,8 @@ var OhMyOpenCodeLite = async (ctx) => {
34278
33851
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
34279
33852
  await interviewManager.handleCommandExecuteBefore(input, output);
34280
33853
  await presetManager.handleCommandExecuteBefore(input, output);
34281
- await sessionGoalHook.handleCommandExecuteBefore(input, output);
33854
+ await goalHook.handleCommandExecuteBefore(input, output);
33855
+ await deepworkCommandHook.handleCommandExecuteBefore(input, output);
34282
33856
  },
34283
33857
  "chat.headers": chatHeadersHook["chat.headers"],
34284
33858
  "chat.message": async (input, output) => {
@@ -34307,7 +33881,7 @@ var OhMyOpenCodeLite = async (ctx) => {
34307
33881
  ${output.system[0]}` : "");
34308
33882
  }
34309
33883
  }
34310
- sessionGoalHook.handleSystemTransform(input, output);
33884
+ goalHook.handleSystemTransform(input, output);
34311
33885
  collapseSystemInPlace(output.system);
34312
33886
  },
34313
33887
  "experimental.chat.messages.transform": async (input, output) => {