oh-my-opencode-slim 1.0.7 → 1.1.0

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
@@ -18220,6 +18220,12 @@ var CUSTOM_SKILLS = [
18220
18220
  description: "Repository understanding and hierarchical codemap generation",
18221
18221
  allowedAgents: ["orchestrator"],
18222
18222
  sourcePath: "src/skills/codemap"
18223
+ },
18224
+ {
18225
+ name: "clonedeps",
18226
+ description: "Clone important dependency source for local inspection",
18227
+ allowedAgents: ["orchestrator"],
18228
+ sourcePath: "src/skills/clonedeps"
18223
18229
  }
18224
18230
  ];
18225
18231
 
@@ -18393,6 +18399,56 @@ var CouncilConfigSchema = z.object({
18393
18399
  import * as fs from "node:fs";
18394
18400
  import * as path from "node:path";
18395
18401
 
18402
+ // src/utils/compat.ts
18403
+ import { spawn as nodeSpawn } from "node:child_process";
18404
+ import { writeFile as fsWriteFile } from "node:fs/promises";
18405
+ var isBun = typeof globalThis.Bun !== "undefined";
18406
+ function collectStream(stream) {
18407
+ if (!stream)
18408
+ return () => Promise.resolve("");
18409
+ const chunks = [];
18410
+ stream.on("data", (chunk) => chunks.push(chunk));
18411
+ return () => new Promise((resolve, reject) => {
18412
+ if (!stream.readable) {
18413
+ resolve(Buffer.concat(chunks).toString("utf-8"));
18414
+ return;
18415
+ }
18416
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
18417
+ stream.on("error", reject);
18418
+ });
18419
+ }
18420
+ function crossSpawn(command, options) {
18421
+ const [cmd, ...args] = command;
18422
+ const proc = nodeSpawn(cmd, args, {
18423
+ stdio: [
18424
+ options?.stdin ?? "ignore",
18425
+ options?.stdout ?? "pipe",
18426
+ options?.stderr ?? "pipe"
18427
+ ],
18428
+ cwd: options?.cwd,
18429
+ env: options?.env
18430
+ });
18431
+ const stdoutCollector = collectStream(proc.stdout);
18432
+ const stderrCollector = collectStream(proc.stderr);
18433
+ const exited = new Promise((resolve, reject) => {
18434
+ proc.on("error", reject);
18435
+ proc.on("close", (code) => resolve(code ?? 1));
18436
+ });
18437
+ return {
18438
+ proc,
18439
+ stdout: stdoutCollector,
18440
+ stderr: stderrCollector,
18441
+ exited,
18442
+ kill: (signal) => proc.kill(signal),
18443
+ get exitCode() {
18444
+ return proc.exitCode;
18445
+ }
18446
+ };
18447
+ }
18448
+ async function crossWrite(path, data) {
18449
+ await fsWriteFile(path, Buffer.from(data));
18450
+ }
18451
+
18396
18452
  // src/config/agent-mcps.ts
18397
18453
  var DEFAULT_AGENT_MCPS = {
18398
18454
  orchestrator: ["*", "!context7"],
@@ -18842,13 +18898,16 @@ function parseModelReference(model) {
18842
18898
  modelID: model.slice(slashIndex + 1)
18843
18899
  };
18844
18900
  }
18845
- async function promptWithTimeout(client, args, timeoutMs) {
18901
+ async function promptWithTimeout(client, args, timeoutMs, signal) {
18902
+ if (signal?.aborted)
18903
+ throw new Error("Prompt cancelled");
18846
18904
  if (timeoutMs <= 0) {
18847
18905
  await client.session.prompt(args);
18848
18906
  return;
18849
18907
  }
18850
18908
  const sessionId = args.path.id;
18851
18909
  let timer;
18910
+ let onAbort;
18852
18911
  try {
18853
18912
  const promptPromise = client.session.prompt(args);
18854
18913
  promptPromise.catch(() => {});
@@ -18858,6 +18917,16 @@ async function promptWithTimeout(client, args, timeoutMs) {
18858
18917
  timer = setTimeout(() => {
18859
18918
  reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
18860
18919
  }, timeoutMs);
18920
+ }),
18921
+ new Promise((_, reject) => {
18922
+ if (!signal)
18923
+ return;
18924
+ if (signal.aborted) {
18925
+ reject(new Error("Prompt cancelled"));
18926
+ return;
18927
+ }
18928
+ onAbort = () => reject(new Error("Prompt cancelled"));
18929
+ signal.addEventListener("abort", onAbort, { once: true });
18861
18930
  })
18862
18931
  ]);
18863
18932
  } catch (error) {
@@ -18869,12 +18938,15 @@ async function promptWithTimeout(client, args, timeoutMs) {
18869
18938
  throw error;
18870
18939
  } finally {
18871
18940
  clearTimeout(timer);
18941
+ if (onAbort)
18942
+ signal?.removeEventListener("abort", onAbort);
18872
18943
  }
18873
18944
  }
18874
18945
  async function extractSessionResult(client, sessionId, options) {
18875
18946
  const includeReasoning = options?.includeReasoning ?? true;
18876
18947
  const messagesResult = await client.session.messages({
18877
- path: { id: sessionId }
18948
+ path: { id: sessionId },
18949
+ ...options?.directory ? { query: { directory: options.directory } } : {}
18878
18950
  });
18879
18951
  const messages = messagesResult.data ?? [];
18880
18952
  const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
@@ -19030,6 +19102,25 @@ ${enabledParallelExamples}
19030
19102
 
19031
19103
  Balance: respect dependencies, avoid parallelizing what must be sequential.
19032
19104
 
19105
+ ### Context Isolation
19106
+ If no specialist delegation is needed, consider \`subtask\` before doing
19107
+ context-heavy work directly.
19108
+
19109
+ Ask whether the parent context needs the details or only the result. Use
19110
+ \`subtask\` when the work is bounded, context-heavy, and the parent only needs a
19111
+ compact outcome.
19112
+
19113
+ Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
19114
+ verification across files/logs/messages.
19115
+
19116
+ Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
19117
+ work better handled by a named specialist, or cases where the parent must reason
19118
+ over the details.
19119
+
19120
+ When calling \`subtask\`, give a self-contained prompt with objective,
19121
+ constraints, relevant context, deliverable, and validation. Pass only clearly
19122
+ relevant files. Wait for the summary, then integrate and verify it.
19123
+
19033
19124
  ### OpenCode subagent execution model
19034
19125
  - A delegated specialist runs in a separate child session.
19035
19126
  - Delegation is blocking for the parent at that point: send work out, then continue that line after results return.
@@ -21966,6 +22057,15 @@ var APPLY_PATCH_RESCUE_OPTIONS = {
21966
22057
  prefixSuffix: true,
21967
22058
  lcsRescue: true
21968
22059
  };
22060
+ function replacePatchArgs(output, args, patchText) {
22061
+ const nextArgs = { ...args, patchText };
22062
+ try {
22063
+ output.args = nextArgs;
22064
+ } catch {
22065
+ return false;
22066
+ }
22067
+ return output.args?.patchText === patchText;
22068
+ }
21969
22069
  function createApplyPatchHook(ctx) {
21970
22070
  function logHookStatus(state, data) {
21971
22071
  log(`apply-patch hook ${state}`, data);
@@ -21985,8 +22085,16 @@ function createApplyPatchHook(ctx) {
21985
22085
  try {
21986
22086
  const result = await rewritePatch(root, patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
21987
22087
  if (result.changed) {
21988
- args.patchText = result.patchText;
21989
- logHookStatus("rewrite");
22088
+ if (replacePatchArgs(output, args, result.patchText)) {
22089
+ logHookStatus("rewrite");
22090
+ } else {
22091
+ logHookStatus("skipped", {
22092
+ reason: "readonly output args",
22093
+ failOpen: true,
22094
+ rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
22095
+ rewriteStage: "before-native"
22096
+ });
22097
+ }
21990
22098
  return;
21991
22099
  }
21992
22100
  logHookStatus("unchanged");
@@ -22018,56 +22126,6 @@ function createApplyPatchHook(ctx) {
22018
22126
  }
22019
22127
  };
22020
22128
  }
22021
- // src/utils/compat.ts
22022
- import { spawn as nodeSpawn } from "node:child_process";
22023
- import { writeFile as fsWriteFile } from "node:fs/promises";
22024
- var isBun = typeof globalThis.Bun !== "undefined";
22025
- function collectStream(stream) {
22026
- if (!stream)
22027
- return () => Promise.resolve("");
22028
- const chunks = [];
22029
- stream.on("data", (chunk) => chunks.push(chunk));
22030
- return () => new Promise((resolve, reject) => {
22031
- if (!stream.readable) {
22032
- resolve(Buffer.concat(chunks).toString("utf-8"));
22033
- return;
22034
- }
22035
- stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
22036
- stream.on("error", reject);
22037
- });
22038
- }
22039
- function crossSpawn(command, options) {
22040
- const [cmd, ...args] = command;
22041
- const proc = nodeSpawn(cmd, args, {
22042
- stdio: [
22043
- options?.stdin ?? "ignore",
22044
- options?.stdout ?? "pipe",
22045
- options?.stderr ?? "pipe"
22046
- ],
22047
- cwd: options?.cwd,
22048
- env: options?.env
22049
- });
22050
- const stdoutCollector = collectStream(proc.stdout);
22051
- const stderrCollector = collectStream(proc.stderr);
22052
- const exited = new Promise((resolve, reject) => {
22053
- proc.on("error", reject);
22054
- proc.on("close", (code) => resolve(code ?? 1));
22055
- });
22056
- return {
22057
- proc,
22058
- stdout: stdoutCollector,
22059
- stderr: stderrCollector,
22060
- exited,
22061
- kill: (signal) => proc.kill(signal),
22062
- get exitCode() {
22063
- return proc.exitCode;
22064
- }
22065
- };
22066
- }
22067
- async function crossWrite(path6, data) {
22068
- await fsWriteFile(path6, Buffer.from(data));
22069
- }
22070
-
22071
22129
  // src/hooks/auto-update-checker/cache.ts
22072
22130
  import * as fs5 from "node:fs";
22073
22131
  import * as path8 from "node:path";
@@ -28644,7 +28702,11 @@ class TmuxMultiplexer {
28644
28702
  this.storedLayout = layout;
28645
28703
  this.storedMainPaneSize = mainPaneSize;
28646
28704
  try {
28647
- const layoutResult = await this.runTmux(tmux, ["select-layout", ...this.targetArgs(), layout]);
28705
+ const layoutResult = await this.runTmux(tmux, [
28706
+ "select-layout",
28707
+ ...this.targetArgs(),
28708
+ layout
28709
+ ]);
28648
28710
  if (layoutResult !== 0)
28649
28711
  return;
28650
28712
  if (layout === "main-horizontal" || layout === "main-vertical") {
@@ -28657,7 +28719,11 @@ class TmuxMultiplexer {
28657
28719
  ]);
28658
28720
  if (sizeResult !== 0)
28659
28721
  return;
28660
- const reapplyResult = await this.runTmux(tmux, ["select-layout", ...this.targetArgs(), layout]);
28722
+ const reapplyResult = await this.runTmux(tmux, [
28723
+ "select-layout",
28724
+ ...this.targetArgs(),
28725
+ layout
28726
+ ]);
28661
28727
  if (reapplyResult !== 0)
28662
28728
  return;
28663
28729
  }
@@ -32572,6 +32638,460 @@ function createWebfetchTool(pluginCtx, options = {}) {
32572
32638
  }
32573
32639
  });
32574
32640
  }
32641
+ // src/tools/subtask/command.ts
32642
+ var COMMAND_NAME4 = "subtask";
32643
+ var SUBTASK_COMMAND_TEMPLATE = `Start a focused subtask worker.
32644
+
32645
+ The user's request below is the full scope for the worker. Do not broaden it.
32646
+ Create a self-contained worker prompt that includes:
32647
+ - the exact objective
32648
+ - relevant context from this conversation
32649
+ - specific files/paths that matter
32650
+ - expected deliverables
32651
+ - validation the worker should run, if applicable
32652
+
32653
+ USER REQUEST:
32654
+ $ARGUMENTS
32655
+
32656
+ Then call the subtask tool:
32657
+ \`subtask(prompt="...", files=["src/foo.ts", "docs/bar.md"])\`
32658
+
32659
+ Only include files that are clearly relevant. If no files are needed, omit files.`;
32660
+ function createSubtaskCommandManager(_ctx, state) {
32661
+ function registerCommand(opencodeConfig) {
32662
+ const configCommand = opencodeConfig.command;
32663
+ if (!configCommand?.[COMMAND_NAME4]) {
32664
+ if (!opencodeConfig.command) {
32665
+ opencodeConfig.command = {};
32666
+ }
32667
+ opencodeConfig.command[COMMAND_NAME4] = {
32668
+ description: "Create a focused subtask prompt for a new session",
32669
+ template: SUBTASK_COMMAND_TEMPLATE
32670
+ };
32671
+ }
32672
+ }
32673
+ return {
32674
+ registerCommand,
32675
+ handleEvent(input) {
32676
+ if (input.event.type === "session.created") {
32677
+ const info = input.event.properties?.info;
32678
+ if (!info?.id || !info.parentID)
32679
+ return;
32680
+ const source = state.sourceFor(info.parentID);
32681
+ if (source)
32682
+ state.markSession(info.id, source);
32683
+ return;
32684
+ }
32685
+ if (input.event.type !== "session.deleted")
32686
+ return;
32687
+ const sessionID = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
32688
+ if (sessionID)
32689
+ state.unmarkSession(sessionID);
32690
+ }
32691
+ };
32692
+ }
32693
+ // src/tools/subtask/files.ts
32694
+ import * as fs11 from "node:fs/promises";
32695
+ import * as path20 from "node:path";
32696
+
32697
+ // src/tools/subtask/vendor.ts
32698
+ import * as fs10 from "node:fs/promises";
32699
+ import * as path19 from "node:path";
32700
+ var DEFAULT_READ_LIMIT = 2000;
32701
+ var MAX_LINE_LENGTH = 2000;
32702
+ var MAX_BYTES = 50 * 1024;
32703
+ var MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
32704
+ var MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`;
32705
+ var SAMPLE_BYTES = 4096;
32706
+ var BINARY_EXTENSIONS = new Set([
32707
+ ".zip",
32708
+ ".tar",
32709
+ ".gz",
32710
+ ".exe",
32711
+ ".dll",
32712
+ ".so",
32713
+ ".class",
32714
+ ".jar",
32715
+ ".war",
32716
+ ".7z",
32717
+ ".doc",
32718
+ ".docx",
32719
+ ".xls",
32720
+ ".xlsx",
32721
+ ".ppt",
32722
+ ".pptx",
32723
+ ".odt",
32724
+ ".ods",
32725
+ ".odp",
32726
+ ".bin",
32727
+ ".dat",
32728
+ ".obj",
32729
+ ".o",
32730
+ ".a",
32731
+ ".lib",
32732
+ ".wasm",
32733
+ ".pyc",
32734
+ ".pyo"
32735
+ ]);
32736
+ async function isBinaryFile(filepath) {
32737
+ const ext = path19.extname(filepath).toLowerCase();
32738
+ if (BINARY_EXTENSIONS.has(ext)) {
32739
+ return true;
32740
+ }
32741
+ try {
32742
+ const file = await fs10.open(filepath, "r");
32743
+ try {
32744
+ const buffer = Buffer.alloc(SAMPLE_BYTES);
32745
+ const result = await file.read(buffer, 0, SAMPLE_BYTES, 0);
32746
+ if (result.bytesRead === 0)
32747
+ return false;
32748
+ const bytes = buffer.subarray(0, result.bytesRead);
32749
+ let nonPrintableCount = 0;
32750
+ for (let i = 0;i < bytes.length; i++) {
32751
+ const byte = bytes[i];
32752
+ if (byte === undefined)
32753
+ continue;
32754
+ if (byte === 0)
32755
+ return true;
32756
+ if (byte < 9 || byte > 13 && byte < 32) {
32757
+ nonPrintableCount++;
32758
+ }
32759
+ }
32760
+ return nonPrintableCount / bytes.length > 0.3;
32761
+ } finally {
32762
+ await file.close();
32763
+ }
32764
+ } catch {
32765
+ return false;
32766
+ }
32767
+ }
32768
+ function formatFileContent(_filepath, content) {
32769
+ const cappedContent = Buffer.byteLength(content, "utf8") > MAX_BYTES;
32770
+ const contentToFormat = cappedContent ? content.slice(0, MAX_BYTES) : content;
32771
+ const lines = contentToFormat.split(`
32772
+ `);
32773
+ const limit = DEFAULT_READ_LIMIT;
32774
+ const offset = 0;
32775
+ const raw = lines.slice(offset, offset + limit).map((line) => {
32776
+ return line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}${MAX_LINE_SUFFIX}` : line;
32777
+ });
32778
+ const formatted = raw.map((line, index) => {
32779
+ return `${index + offset + 1}: ${line}`;
32780
+ });
32781
+ let output = [
32782
+ `<path>${_filepath}</path>`,
32783
+ "<type>file</type>",
32784
+ `<content>
32785
+ `
32786
+ ].join(`
32787
+ `);
32788
+ output += formatted.join(`
32789
+ `);
32790
+ const totalLines = lines.length;
32791
+ const lastReadLine = offset + formatted.length;
32792
+ const hasMoreLines = totalLines > lastReadLine;
32793
+ if (cappedContent) {
32794
+ output += `
32795
+
32796
+ (Output capped at ${MAX_BYTES_LABEL}. Showing lines 1-${lastReadLine}. Use offset=${lastReadLine + 1} to continue.)`;
32797
+ } else if (hasMoreLines) {
32798
+ output += `
32799
+
32800
+ (Showing lines 1-${lastReadLine} of ${totalLines}. Use offset=${lastReadLine + 1} to continue.)`;
32801
+ } else {
32802
+ output += `
32803
+
32804
+ (End of file - total ${totalLines} lines)`;
32805
+ }
32806
+ output += `
32807
+ </content>`;
32808
+ return output;
32809
+ }
32810
+
32811
+ // src/tools/subtask/files.ts
32812
+ var FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
32813
+ var TRAILING_PATH_PUNCTUATION = /[!?:;]+$/;
32814
+ function cleanFileReference(ref) {
32815
+ return ref.replace(/^@/, "").replace(TRAILING_PATH_PUNCTUATION, "");
32816
+ }
32817
+ function parseFileReferences(text) {
32818
+ const fileRefs = new Set;
32819
+ for (const match of text.matchAll(FILE_REGEX)) {
32820
+ if (match[1]) {
32821
+ fileRefs.add(cleanFileReference(match[1]));
32822
+ }
32823
+ }
32824
+ return fileRefs;
32825
+ }
32826
+ async function buildSyntheticFileParts(directory, refs) {
32827
+ const parts = [];
32828
+ const realDirectory = await fs11.realpath(directory);
32829
+ for (const ref of refs) {
32830
+ const filepath = path20.resolve(directory, ref);
32831
+ const relative3 = path20.relative(directory, filepath);
32832
+ if (relative3.startsWith("..") || path20.isAbsolute(relative3))
32833
+ continue;
32834
+ try {
32835
+ const realFilepath = await fs11.realpath(filepath);
32836
+ const realRelative = path20.relative(realDirectory, realFilepath);
32837
+ if (realRelative.startsWith("..") || path20.isAbsolute(realRelative)) {
32838
+ continue;
32839
+ }
32840
+ const stats = await fs11.stat(realFilepath);
32841
+ if (!stats.isFile())
32842
+ continue;
32843
+ if (await isBinaryFile(realFilepath))
32844
+ continue;
32845
+ const content = await fs11.readFile(realFilepath, "utf-8");
32846
+ parts.push({
32847
+ type: "text",
32848
+ synthetic: true,
32849
+ text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: realFilepath })}`
32850
+ });
32851
+ parts.push({
32852
+ type: "text",
32853
+ synthetic: true,
32854
+ text: formatFileContent(realFilepath, content)
32855
+ });
32856
+ } catch {}
32857
+ }
32858
+ return parts;
32859
+ }
32860
+ // src/tools/subtask/state.ts
32861
+ function createSubtaskState() {
32862
+ const sourceBySession = new Map;
32863
+ return {
32864
+ markSession(sessionID, sourceSessionID) {
32865
+ sourceBySession.set(sessionID, sourceSessionID);
32866
+ },
32867
+ unmarkSession(sessionID) {
32868
+ sourceBySession.delete(sessionID);
32869
+ },
32870
+ isSubtaskSession(sessionID) {
32871
+ return sourceBySession.has(sessionID);
32872
+ },
32873
+ sourceFor(sessionID) {
32874
+ return sourceBySession.get(sessionID);
32875
+ }
32876
+ };
32877
+ }
32878
+ // src/tools/subtask/tools.ts
32879
+ import { tool as tool5 } from "@opencode-ai/plugin";
32880
+ var SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
32881
+ var SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
32882
+ function normalizeSubtaskSummary(text) {
32883
+ return text.replace(SUBTASK_SUMMARY_TAG_REGEX, "").trim();
32884
+ }
32885
+ function getAbortSignal(context) {
32886
+ if (!context || typeof context !== "object" || !("abort" in context)) {
32887
+ return;
32888
+ }
32889
+ const signal = context.abort;
32890
+ return signal && typeof signal === "object" && "addEventListener" in signal && "removeEventListener" in signal && "aborted" in signal ? signal : undefined;
32891
+ }
32892
+ function createSubtaskTool(ctx, state, depthTracker) {
32893
+ const client = ctx.client;
32894
+ return tool5({
32895
+ description: "Run a child worker session and return its completion summary to the caller",
32896
+ args: {
32897
+ prompt: tool5.schema.string().describe("The generated subtask prompt"),
32898
+ files: tool5.schema.array(tool5.schema.string()).optional().describe("Array of file paths to load into the new session's context")
32899
+ },
32900
+ async execute(args, context) {
32901
+ const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : ctx.directory;
32902
+ const sessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : "unknown";
32903
+ const abortSignal = getAbortSignal(context);
32904
+ if (state.isSubtaskSession(sessionID)) {
32905
+ return "Nested subtask is disabled: this session is already a subtask worker. Finish this worker and return its summary to the parent session instead.";
32906
+ }
32907
+ if (sessionID !== "unknown" && depthTracker && depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth) {
32908
+ return `Subtask worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
32909
+ }
32910
+ const sessionReference = `You are a subtask worker spawned by parent session ${sessionID}.
32911
+
32912
+ Your job is bounded: complete only the task below. Do not expand scope.
32913
+ If needed context is missing, use read_session to inspect the parent session.
32914
+ Do not spawn another subtask.`;
32915
+ const files = new Set([
32916
+ ...parseFileReferences(args.prompt),
32917
+ ...(args.files ?? []).map(cleanFileReference)
32918
+ ]);
32919
+ const fileRefs = files.size > 0 ? [...files].map((f) => `@${f}`).join(" ") : "";
32920
+ const fullPrompt = fileRefs ? `${sessionReference}
32921
+
32922
+ TASK:
32923
+ ${args.prompt}
32924
+
32925
+ FILES PROVIDED:
32926
+ ${fileRefs}` : `${sessionReference}
32927
+
32928
+ TASK:
32929
+ ${args.prompt}`;
32930
+ let childSessionID;
32931
+ try {
32932
+ const session2 = await client.session.create({
32933
+ responseStyle: "data",
32934
+ throwOnError: true,
32935
+ query: { directory },
32936
+ body: {
32937
+ parentID: sessionID === "unknown" ? undefined : sessionID,
32938
+ title: `Subtask worker from ${sessionID}`
32939
+ }
32940
+ });
32941
+ childSessionID = session2?.data?.id ?? session2?.id;
32942
+ if (!childSessionID) {
32943
+ throw new Error("Subtask worker session did not return an id");
32944
+ }
32945
+ if (sessionID !== "unknown" && depthTracker) {
32946
+ const registered = depthTracker.registerChild(sessionID, childSessionID);
32947
+ if (!registered) {
32948
+ throw new Error("Subtask worker blocked: max subagent depth exceeded");
32949
+ }
32950
+ }
32951
+ state.markSession(childSessionID, sessionID);
32952
+ await promptWithTimeout(client, {
32953
+ responseStyle: "data",
32954
+ throwOnError: true,
32955
+ query: { directory },
32956
+ path: { id: childSessionID },
32957
+ body: {
32958
+ agent: "orchestrator",
32959
+ parts: [
32960
+ {
32961
+ type: "text",
32962
+ text: `${fullPrompt}
32963
+
32964
+ Instructions:
32965
+ 1. Understand the task and relevant file context.
32966
+ 2. Make only necessary changes.
32967
+ 3. Run the most relevant validation checks when practical.
32968
+ 4. Stop when the requested task is done.
32969
+
32970
+ Return your final response in this format:
32971
+
32972
+ <subtask_summary>
32973
+ Status: completed | blocked | partial
32974
+
32975
+ What changed:
32976
+ - ...
32977
+
32978
+ Files touched:
32979
+ - ...
32980
+
32981
+ Validation:
32982
+ - ...
32983
+
32984
+ Risks / follow-up:
32985
+ - ...
32986
+ </subtask_summary>`
32987
+ },
32988
+ ...await buildSyntheticFileParts(directory, files)
32989
+ ]
32990
+ }
32991
+ }, SUBTASK_TIMEOUT_MS, abortSignal);
32992
+ const extraction = await extractSessionResult(client, childSessionID, {
32993
+ directory,
32994
+ includeReasoning: false
32995
+ });
32996
+ if (extraction.empty) {
32997
+ throw new Error("Subtask worker returned no summary");
32998
+ }
32999
+ const summary = normalizeSubtaskSummary(extraction.text);
33000
+ return [
33001
+ `task_id: ${childSessionID}`,
33002
+ "",
33003
+ "<subtask_summary>",
33004
+ summary,
33005
+ "</subtask_summary>"
33006
+ ].join(`
33007
+ `);
33008
+ } finally {
33009
+ if (childSessionID) {
33010
+ try {
33011
+ await client.session.abort({
33012
+ path: { id: childSessionID },
33013
+ query: { directory }
33014
+ });
33015
+ state.unmarkSession(childSessionID);
33016
+ } catch {}
33017
+ }
33018
+ }
33019
+ }
33020
+ });
33021
+ }
33022
+ function formatTranscript(messages, limit) {
33023
+ const lines = [];
33024
+ for (const msg of messages) {
33025
+ const role = msg.info?.role;
33026
+ const parts = msg.parts;
33027
+ if (role === "user") {
33028
+ lines.push("## User");
33029
+ for (const part of parts) {
33030
+ if (part.type === "text" && !part.ignored && typeof part.text === "string") {
33031
+ lines.push(part.text);
33032
+ }
33033
+ if (part.type === "file") {
33034
+ lines.push(`[Attached: ${part.filename || "file"}]`);
33035
+ }
33036
+ }
33037
+ lines.push("");
33038
+ }
33039
+ if (role === "assistant") {
33040
+ lines.push("## Assistant");
33041
+ for (const part of parts) {
33042
+ if (part.type === "text" && typeof part.text === "string") {
33043
+ lines.push(part.text);
33044
+ }
33045
+ if (part.type === "tool" && part.state?.status === "completed" && part.tool) {
33046
+ lines.push(`[Tool: ${part.tool}] ${part.state.title ?? ""}`);
33047
+ }
33048
+ }
33049
+ lines.push("");
33050
+ }
33051
+ }
33052
+ const output = lines.join(`
33053
+ `).trim();
33054
+ if (messages.length >= (limit ?? 100)) {
33055
+ return output + `
33056
+
33057
+ (Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`;
33058
+ }
33059
+ return `${output}
33060
+
33061
+ (End of session - ${messages.length} messages)`;
33062
+ }
33063
+ function createReadSessionTool(client, state) {
33064
+ return tool5({
33065
+ 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.",
33066
+ args: {
33067
+ sessionID: tool5.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
33068
+ limit: tool5.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)")
33069
+ },
33070
+ async execute(args, context) {
33071
+ const limit = Math.min(args.limit ?? 100, 500);
33072
+ const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : undefined;
33073
+ const callerSessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : undefined;
33074
+ if (!callerSessionID || !state.isSubtaskSession(callerSessionID)) {
33075
+ return "read_session is only available from subtask worker sessions.";
33076
+ }
33077
+ if (state.sourceFor(callerSessionID) !== args.sessionID) {
33078
+ return "read_session can only read the source session for this subtask worker.";
33079
+ }
33080
+ try {
33081
+ const response = await client.session.messages({
33082
+ path: { id: args.sessionID },
33083
+ query: { limit, ...directory ? { directory } : {} }
33084
+ });
33085
+ if (!response.data || response.data.length === 0) {
33086
+ return "Session has no messages or does not exist.";
33087
+ }
33088
+ return formatTranscript(response.data, limit);
33089
+ } catch (error) {
33090
+ return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : "Unknown error"}`;
33091
+ }
33092
+ }
33093
+ });
33094
+ }
32575
33095
  // src/utils/subagent-depth.ts
32576
33096
  class SubagentDepthTracker {
32577
33097
  depthBySession = new Map;
@@ -32691,6 +33211,8 @@ var OhMyOpenCodeLite = async (ctx) => {
32691
33211
  let councilTools;
32692
33212
  let webfetch;
32693
33213
  let rewriteDisplayNameMentions;
33214
+ let subtaskCommandManager;
33215
+ let subtaskState;
32694
33216
  let toolCount = 0;
32695
33217
  try {
32696
33218
  config = loadPluginConfig(ctx.directory);
@@ -32783,7 +33305,9 @@ var OhMyOpenCodeLite = async (ctx) => {
32783
33305
  interviewManager = createInterviewManager(ctx, config);
32784
33306
  presetManager = createPresetManager(ctx, config);
32785
33307
  divoomManager = createDivoomManager(config.divoom);
32786
- toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
33308
+ subtaskState = createSubtaskState();
33309
+ subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
33310
+ toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2 + 2;
32787
33311
  } catch (err) {
32788
33312
  log("[plugin] FATAL: init failed", String(err));
32789
33313
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -32828,7 +33352,9 @@ var OhMyOpenCodeLite = async (ctx) => {
32828
33352
  webfetch,
32829
33353
  ...todoContinuationHook.tool,
32830
33354
  ast_grep_search,
32831
- ast_grep_replace
33355
+ ast_grep_replace,
33356
+ subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
33357
+ read_session: createReadSessionTool(ctx.client, subtaskState)
32832
33358
  },
32833
33359
  mcp: mcps,
32834
33360
  config: async (opencodeConfig) => {
@@ -33025,6 +33551,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33025
33551
  }
33026
33552
  interviewManager.registerCommand(opencodeConfig);
33027
33553
  presetManager.registerCommand(opencodeConfig);
33554
+ subtaskCommandManager.registerCommand(opencodeConfig);
33028
33555
  },
33029
33556
  event: async (input) => {
33030
33557
  const event = input.event;
@@ -33052,6 +33579,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33052
33579
  await multiplexerSessionManager.onSessionDeleted(event);
33053
33580
  await interviewManager.handleEvent(input);
33054
33581
  await taskSessionManagerHook.event(input);
33582
+ subtaskCommandManager.handleEvent(input);
33055
33583
  if (event.type === "permission.asked" || event.type === "question.asked") {
33056
33584
  const props = event.properties;
33057
33585
  divoomManager.onUserInputRequired({