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/README.md +9 -4
- package/dist/cli/config-io.d.ts +1 -0
- package/dist/cli/index.js +170 -52
- package/dist/index.js +586 -58
- package/dist/tools/fork/command.d.ts +28 -0
- package/dist/tools/fork/files.d.ts +33 -0
- package/dist/tools/fork/index.d.ts +10 -0
- package/dist/tools/fork/state.d.ts +7 -0
- package/dist/tools/fork/tools.d.ts +23 -0
- package/dist/tools/fork/vendor.d.ts +28 -0
- package/dist/tools/handoff/command.d.ts +29 -0
- package/dist/tools/handoff/files.d.ts +33 -0
- package/dist/tools/handoff/index.d.ts +10 -0
- package/dist/tools/handoff/state.d.ts +7 -0
- package/dist/tools/handoff/tools.d.ts +23 -0
- package/dist/tools/handoff/vendor.d.ts +28 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/subtask/command.d.ts +30 -0
- package/dist/tools/subtask/files.d.ts +34 -0
- package/dist/tools/subtask/index.d.ts +11 -0
- package/dist/tools/subtask/state.d.ts +7 -0
- package/dist/tools/subtask/tools.d.ts +23 -0
- package/dist/tools/subtask/vendor.d.ts +27 -0
- package/dist/tui.js +56 -0
- package/dist/utils/session.d.ts +2 -1
- package/package.json +1 -1
- package/src/skills/clonedeps/README.md +23 -0
- package/src/skills/clonedeps/SKILL.md +237 -0
- package/src/skills/clonedeps/codemap.md +41 -0
- package/src/skills/codemap.md +8 -5
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
|
|
21989
|
-
|
|
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, [
|
|
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, [
|
|
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
|
-
|
|
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({
|