oh-my-opencode-slim 2.0.0-beta.1 → 2.0.0-beta.4
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 +2 -2
- package/dist/agents/orchestrator.d.ts +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/hooks/deepwork/index.d.ts +13 -0
- package/dist/hooks/{session-goal → goal}/index.d.ts +1 -1
- package/dist/hooks/index.d.ts +2 -1
- package/dist/index.js +507 -932
- package/dist/tools/index.d.ts +0 -2
- package/dist/tui.js +6 -0
- package/package.json +3 -2
- package/src/skills/codemap.md +3 -2
- package/src/skills/deepwork/SKILL.md +92 -0
- package/dist/agents/council-master.d.ts +0 -2
- package/dist/background/background-manager.d.ts +0 -203
- package/dist/background/index.d.ts +0 -3
- package/dist/background/multiplexer-session-manager.d.ts +0 -70
- package/dist/background/subagent-depth.d.ts +0 -35
- package/dist/cli/divoom.d.ts +0 -23
- package/dist/goal/index.d.ts +0 -3
- package/dist/goal/manager.d.ts +0 -41
- package/dist/goal/prompts.d.ts +0 -4
- package/dist/goal/store.d.ts +0 -15
- package/dist/goal/types.d.ts +0 -28
- package/dist/integrations/divoom/index.d.ts +0 -3
- package/dist/integrations/divoom/status-manager.d.ts +0 -31
- package/dist/integrations/divoom/swift-helper-source.d.ts +0 -1
- package/dist/integrations/divoom/swift-transport.d.ts +0 -26
- package/dist/integrations/divoom/types.d.ts +0 -41
- package/dist/tools/background.d.ts +0 -13
- package/dist/tools/fork/command.d.ts +0 -28
- package/dist/tools/fork/files.d.ts +0 -33
- package/dist/tools/fork/index.d.ts +0 -10
- package/dist/tools/fork/state.d.ts +0 -7
- package/dist/tools/fork/tools.d.ts +0 -23
- package/dist/tools/fork/vendor.d.ts +0 -28
- package/dist/tools/handoff/command.d.ts +0 -29
- package/dist/tools/handoff/files.d.ts +0 -33
- package/dist/tools/handoff/index.d.ts +0 -10
- package/dist/tools/handoff/state.d.ts +0 -7
- package/dist/tools/handoff/tools.d.ts +0 -23
- package/dist/tools/handoff/vendor.d.ts +0 -28
- package/dist/tools/lsp/client.d.ts +0 -81
- package/dist/tools/lsp/config-store.d.ts +0 -29
- package/dist/tools/lsp/config.d.ts +0 -5
- package/dist/tools/lsp/constants.d.ts +0 -24
- package/dist/tools/lsp/index.d.ts +0 -4
- package/dist/tools/lsp/tools.d.ts +0 -5
- package/dist/tools/lsp/types.d.ts +0 -45
- package/dist/tools/lsp/utils.d.ts +0 -34
- package/dist/tools/subtask/command.d.ts +0 -30
- package/dist/tools/subtask/files.d.ts +0 -34
- package/dist/tools/subtask/index.d.ts +0 -11
- package/dist/tools/subtask/state.d.ts +0 -7
- package/dist/tools/subtask/tools.d.ts +0 -23
- package/dist/tools/subtask/vendor.d.ts +0 -27
- 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
|
|
|
@@ -18966,14 +18972,14 @@ var AGENT_DESCRIPTIONS = {
|
|
|
18966
18972
|
- **Delegate when:** Need to discover what exists before planning • Parallel searches speed discovery • Need summarized map vs full contents • Broad/uncertain scope
|
|
18967
18973
|
- **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
|
|
18968
18974
|
librarian: `@librarian
|
|
18969
|
-
- Lane: External knowledge and library research
|
|
18970
|
-
- Role: Authoritative source for current library docs and API references
|
|
18975
|
+
- Lane: External knowledge and library research, fast web research
|
|
18976
|
+
- Role: Authoritative source for current library docs and API references, web information retrieval
|
|
18971
18977
|
- Permissions: External docs/search MCPs; no file edits
|
|
18972
18978
|
- Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
|
|
18973
18979
|
- Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
|
|
18974
|
-
- **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices
|
|
18980
|
+
- **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices • Working on fixing tricky bug or problem and need latest web research information
|
|
18975
18981
|
- **Don't delegate when:** Standard usage you're confident • Simple stable APIs • General programming knowledge • Info already in conversation • Built-in language features
|
|
18976
|
-
- **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → answer directly.`,
|
|
18982
|
+
- **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → answer directly. How does others solve or workaround this tricky issue?" → @librarian.`,
|
|
18977
18983
|
oracle: `@oracle
|
|
18978
18984
|
- Lane: Architecture, risk, debugging strategy, and review
|
|
18979
18985
|
- Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
|
|
@@ -18984,23 +18990,24 @@ var AGENT_DESCRIPTIONS = {
|
|
|
18984
18990
|
- **Don't delegate when:** Routine decisions you're confident about • First bug fix attempt • Straightforward trade-offs • Tactical "how" vs strategic "should" • Time-sensitive good-enough decisions • Quick research/testing can answer
|
|
18985
18991
|
- **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Routine coordination or final synthesis? → handle directly.`,
|
|
18986
18992
|
designer: `@designer
|
|
18987
|
-
- Lane: User-facing UI/UX design, polish, and review
|
|
18993
|
+
- Lane: User-facing UI/UX design related edits, polish, and review
|
|
18988
18994
|
- Role: UI/UX specialist for intentional, polished experiences
|
|
18989
18995
|
- Permissions: Read/write files
|
|
18990
18996
|
- Stats: 10x better UI/UX than orchestrator
|
|
18991
18997
|
- Capabilities: Visual relevant edits, interactions, responsive layouts, design systems with aesthetic intent, deep UI/UX knowledge.
|
|
18998
|
+
- Weakness: copywriting, needs Orchestrator control, dictation, reviews
|
|
18992
18999
|
- **Delegate when:** User-facing interfaces needing polish • Responsive layouts • UX-critical components (forms, nav, dashboards) • Visual consistency systems • Animations/micro-interactions • Landing/marketing pages • Refining functional→delightful • Reviewing existing UI/UX quality
|
|
18993
19000
|
- **Don't delegate when:** Backend/logic with no visual • Quick prototypes where design doesn't matter yet
|
|
18994
19001
|
- **Rule of thumb:** Users see it and polish matters? → @designer. Headless/functional implementation? → schedule @fixer.`,
|
|
18995
19002
|
fixer: `@fixer
|
|
18996
|
-
- Lane: Bounded implementation and test execution
|
|
18997
|
-
- Role: Fast execution specialist for well-defined tasks
|
|
19003
|
+
- Lane: Bounded implementation and test execution.
|
|
19004
|
+
- Role: Fast execution specialist for well-defined tasks.
|
|
18998
19005
|
- Permissions: Read/write files
|
|
18999
19006
|
- Stats: 2x faster code edits, 1/2 cost of orchestrator, 0.8x quality of orchestrator
|
|
19000
19007
|
- Tools/Constraints: Execution-focused—no research, no architectural decisions
|
|
19001
19008
|
- **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer • Writing or updating tests • Tasks that touch test files, fixtures, mocks, or test helpers. Parallelization benefits: Task involves multiple folders and multiple files modificaiton, scoping work per folder and spawning parallel @fixers for each folder.
|
|
19002
19009
|
- **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to fixer > doing • Tight integration with your current work • Sequential dependencies
|
|
19003
|
-
- **Rule of thumb:**
|
|
19010
|
+
- **Rule of thumb:** Implementation are needed, schedule @fixer with clear scope. Bigger or lots of edits should be split by ownership and dispatched as parallel background fixer lanes when safe.`,
|
|
19004
19011
|
council: `@council
|
|
19005
19012
|
- Lane: High-stakes multi-model decision support
|
|
19006
19013
|
- Role: Multi-LLM consensus engine that runs several councillors, synthesizes their views, and returns a structured council report.
|
|
@@ -19026,7 +19033,7 @@ var AGENT_DESCRIPTIONS = {
|
|
|
19026
19033
|
var VALIDATION_ROUTING = [
|
|
19027
19034
|
"- Route UI/UX validation and review to @designer",
|
|
19028
19035
|
"- Route code review, simplification, maintainability review, and YAGNI checks to @oracle",
|
|
19029
|
-
"- Route
|
|
19036
|
+
"- Route implementation to @fixer or multiple @fixer instances for maximum parallel execution",
|
|
19030
19037
|
"- Route visual/media analysis and interpretation to @observer",
|
|
19031
19038
|
"- If a request spans multiple lanes, delegate only the lanes that add clear value"
|
|
19032
19039
|
];
|
|
@@ -19101,27 +19108,6 @@ ${enabledParallelExamples}
|
|
|
19101
19108
|
|
|
19102
19109
|
Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
|
|
19103
19110
|
|
|
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
19111
|
### OpenCode scheduler model
|
|
19126
19112
|
- Delegated specialists should be launched as background tasks whenever work can run independently: use \`task(..., background: true)\`.
|
|
19127
19113
|
- A dispatch returns a task/session ID immediately; it does not mean completion.
|
|
@@ -23207,6 +23193,53 @@ function createChatHeadersHook(ctx) {
|
|
|
23207
23193
|
}
|
|
23208
23194
|
};
|
|
23209
23195
|
}
|
|
23196
|
+
// src/hooks/deepwork/index.ts
|
|
23197
|
+
var COMMAND_NAME = "deepwork";
|
|
23198
|
+
function activationPrompt(task2) {
|
|
23199
|
+
return [
|
|
23200
|
+
"Use the deepwork skill for this task. Treat it as a heavy coding session.",
|
|
23201
|
+
"",
|
|
23202
|
+
"Deepwork requirements:",
|
|
23203
|
+
"- create/update a `.slim/deepwork/` progress file;",
|
|
23204
|
+
"- keep OpenCode todos synced with the current phase;",
|
|
23205
|
+
"- draft a plan and get `@oracle` review before implementation;",
|
|
23206
|
+
"- create and review a phased implementation/delegation plan;",
|
|
23207
|
+
"- execute phase by phase with background specialists where useful;",
|
|
23208
|
+
"- poll `task_status`, reconcile results, validate, and ask `@oracle` to review each phase;",
|
|
23209
|
+
"- ask `@oracle` to include simplify/readability feedback in phase reviews;",
|
|
23210
|
+
"- fix actionable review issues before continuing.",
|
|
23211
|
+
"",
|
|
23212
|
+
"Task:",
|
|
23213
|
+
task2
|
|
23214
|
+
].join(`
|
|
23215
|
+
`);
|
|
23216
|
+
}
|
|
23217
|
+
function createDeepworkCommandHook() {
|
|
23218
|
+
return {
|
|
23219
|
+
registerCommand: (opencodeConfig) => {
|
|
23220
|
+
const commandConfig = opencodeConfig.command;
|
|
23221
|
+
if (commandConfig?.[COMMAND_NAME])
|
|
23222
|
+
return;
|
|
23223
|
+
if (!opencodeConfig.command)
|
|
23224
|
+
opencodeConfig.command = {};
|
|
23225
|
+
opencodeConfig.command[COMMAND_NAME] = {
|
|
23226
|
+
template: "Start a deepwork session for a complex coding task",
|
|
23227
|
+
description: "Use the deepwork workflow for heavy multi-phase coding work"
|
|
23228
|
+
};
|
|
23229
|
+
},
|
|
23230
|
+
handleCommandExecuteBefore: async (input, output) => {
|
|
23231
|
+
if (input.command !== COMMAND_NAME)
|
|
23232
|
+
return;
|
|
23233
|
+
output.parts.length = 0;
|
|
23234
|
+
const task2 = input.arguments.trim();
|
|
23235
|
+
if (!task2) {
|
|
23236
|
+
output.parts.push(createInternalAgentTextPart("What task should deepwork manage? Run `/deepwork <task>`."));
|
|
23237
|
+
return;
|
|
23238
|
+
}
|
|
23239
|
+
output.parts.push({ type: "text", text: activationPrompt(task2) });
|
|
23240
|
+
}
|
|
23241
|
+
};
|
|
23242
|
+
}
|
|
23210
23243
|
// src/hooks/delegate-task-retry/patterns.ts
|
|
23211
23244
|
var DELEGATE_TASK_ERROR_PATTERNS = [
|
|
23212
23245
|
{
|
|
@@ -23620,370 +23653,58 @@ class ForegroundFallbackManager {
|
|
|
23620
23653
|
return all;
|
|
23621
23654
|
}
|
|
23622
23655
|
}
|
|
23623
|
-
// src/hooks/
|
|
23624
|
-
import
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
|
|
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") };
|
|
23656
|
+
// src/hooks/goal/index.ts
|
|
23657
|
+
import * as fs7 from "node:fs/promises";
|
|
23658
|
+
|
|
23659
|
+
// src/interview/document.ts
|
|
23660
|
+
import * as fsSync from "node:fs";
|
|
23661
|
+
import * as fs6 from "node:fs/promises";
|
|
23662
|
+
import * as path9 from "node:path";
|
|
23663
|
+
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
23664
|
+
function normalizeOutputFolder(outputFolder) {
|
|
23665
|
+
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
23666
|
+
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
23657
23667
|
}
|
|
23658
|
-
function
|
|
23659
|
-
|
|
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";
|
|
23668
|
+
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
23669
|
+
return path9.join(directory, normalizeOutputFolder(outputFolder));
|
|
23668
23670
|
}
|
|
23669
|
-
function
|
|
23670
|
-
|
|
23671
|
+
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
23672
|
+
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
23673
|
+
return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
23671
23674
|
}
|
|
23672
|
-
function
|
|
23673
|
-
|
|
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
|
-
}
|
|
23675
|
+
function relativeInterviewPath(directory, filePath) {
|
|
23676
|
+
return path9.relative(directory, filePath) || path9.basename(filePath);
|
|
23717
23677
|
}
|
|
23718
|
-
function
|
|
23719
|
-
const
|
|
23720
|
-
|
|
23721
|
-
|
|
23722
|
-
if (existsSync5(candidate)) {
|
|
23723
|
-
return candidate;
|
|
23678
|
+
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
23679
|
+
const trimmed = value.trim();
|
|
23680
|
+
if (!trimmed) {
|
|
23681
|
+
return null;
|
|
23724
23682
|
}
|
|
23725
|
-
|
|
23726
|
-
const
|
|
23727
|
-
|
|
23728
|
-
|
|
23729
|
-
|
|
23730
|
-
|
|
23731
|
-
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
|
|
23735
|
-
continue;
|
|
23736
|
-
}
|
|
23737
|
-
log2(`[image-hook] failed to save image: ${e}`);
|
|
23738
|
-
return null;
|
|
23683
|
+
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
23684
|
+
const candidates = new Set;
|
|
23685
|
+
const resolvedRoot = path9.resolve(directory);
|
|
23686
|
+
if (path9.isAbsolute(trimmed)) {
|
|
23687
|
+
candidates.add(trimmed);
|
|
23688
|
+
} else {
|
|
23689
|
+
candidates.add(path9.resolve(directory, trimmed));
|
|
23690
|
+
candidates.add(path9.join(outputDir, trimmed));
|
|
23691
|
+
if (!trimmed.endsWith(".md")) {
|
|
23692
|
+
candidates.add(path9.join(outputDir, `${trimmed}.md`));
|
|
23739
23693
|
}
|
|
23740
23694
|
}
|
|
23741
|
-
|
|
23742
|
-
|
|
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")
|
|
23695
|
+
for (const candidate of candidates) {
|
|
23696
|
+
if (path9.extname(candidate) !== ".md") {
|
|
23752
23697
|
continue;
|
|
23753
|
-
const imageParts = msg.parts.filter(isImagePart);
|
|
23754
|
-
if (imageParts.length > 0) {
|
|
23755
|
-
messagesWithImages.push({ msg, imageParts });
|
|
23756
23698
|
}
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
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}`);
|
|
23699
|
+
const resolved = path9.resolve(candidate);
|
|
23700
|
+
if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
|
|
23701
|
+
continue;
|
|
23781
23702
|
}
|
|
23782
|
-
|
|
23783
|
-
|
|
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
|
-
}
|
|
23703
|
+
if (fsSync.existsSync(candidate)) {
|
|
23704
|
+
return candidate;
|
|
23799
23705
|
}
|
|
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
23706
|
}
|
|
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;
|
|
23707
|
+
return null;
|
|
23987
23708
|
}
|
|
23988
23709
|
function slugify(value) {
|
|
23989
23710
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
@@ -24104,8 +23825,8 @@ A: ${answer.answer.trim()}` : null;
|
|
|
24104
23825
|
}), "utf8");
|
|
24105
23826
|
}
|
|
24106
23827
|
|
|
24107
|
-
// src/hooks/
|
|
24108
|
-
var
|
|
23828
|
+
// src/hooks/goal/index.ts
|
|
23829
|
+
var COMMAND_NAME2 = "goal";
|
|
24109
23830
|
var MAX_GOAL_LENGTH = 4000;
|
|
24110
23831
|
function normalizeGoalText(text) {
|
|
24111
23832
|
return text.trim().replace(/\s+/g, " ").slice(0, MAX_GOAL_LENGTH);
|
|
@@ -24162,23 +23883,23 @@ function resolveGoal(goals, sessionID) {
|
|
|
24162
23883
|
currentSessionID = goal.inheritedFrom;
|
|
24163
23884
|
}
|
|
24164
23885
|
}
|
|
24165
|
-
function
|
|
23886
|
+
function createGoalHook(ctx, config, options) {
|
|
24166
23887
|
const goals = new Map;
|
|
24167
23888
|
const outputFolder = config.interview?.outputFolder ?? "interview";
|
|
24168
23889
|
return {
|
|
24169
23890
|
registerCommand: (opencodeConfig) => {
|
|
24170
23891
|
const commandConfig = opencodeConfig.command;
|
|
24171
|
-
if (commandConfig?.[
|
|
23892
|
+
if (commandConfig?.[COMMAND_NAME2])
|
|
24172
23893
|
return;
|
|
24173
23894
|
if (!opencodeConfig.command)
|
|
24174
23895
|
opencodeConfig.command = {};
|
|
24175
|
-
opencodeConfig.command[
|
|
24176
|
-
template: "Set or show the current
|
|
23896
|
+
opencodeConfig.command[COMMAND_NAME2] = {
|
|
23897
|
+
template: "Set or show the current goal",
|
|
24177
23898
|
description: "Pin a session objective that keeps todos, delegation, and verification aligned"
|
|
24178
23899
|
};
|
|
24179
23900
|
},
|
|
24180
23901
|
handleCommandExecuteBefore: async (input, output) => {
|
|
24181
|
-
if (input.command !==
|
|
23902
|
+
if (input.command !== COMMAND_NAME2)
|
|
24182
23903
|
return;
|
|
24183
23904
|
output.parts.length = 0;
|
|
24184
23905
|
const args = input.arguments.trim();
|
|
@@ -24190,76 +23911,388 @@ ${resolved.goal.text}
|
|
|
24190
23911
|
Use todos for execution steps. Auto-continuation continues only while todos remain.` : "No active goal. Set one with /goal <objective>.");
|
|
24191
23912
|
return;
|
|
24192
23913
|
}
|
|
24193
|
-
if (args === "clear") {
|
|
24194
|
-
goals.delete(input.sessionID);
|
|
24195
|
-
pushText(output, "Cleared the active goal for this session.");
|
|
23914
|
+
if (args === "clear") {
|
|
23915
|
+
goals.delete(input.sessionID);
|
|
23916
|
+
pushText(output, "Cleared the active goal for this session.");
|
|
23917
|
+
return;
|
|
23918
|
+
}
|
|
23919
|
+
if (args.startsWith("from ")) {
|
|
23920
|
+
const value = args.slice("from ".length).trim();
|
|
23921
|
+
const interviewGoal = await readInterviewGoal(ctx.directory, outputFolder, value);
|
|
23922
|
+
if (!interviewGoal) {
|
|
23923
|
+
pushText(output, `Could not find a readable interview spec for "${value}".`);
|
|
23924
|
+
return;
|
|
23925
|
+
}
|
|
23926
|
+
goals.set(input.sessionID, {
|
|
23927
|
+
text: interviewGoal.text,
|
|
23928
|
+
source: "interview",
|
|
23929
|
+
sourcePath: interviewGoal.sourcePath,
|
|
23930
|
+
createdAt: Date.now()
|
|
23931
|
+
});
|
|
23932
|
+
pushText(output, `Set active goal from interview:
|
|
23933
|
+
${interviewGoal.text}`);
|
|
23934
|
+
return;
|
|
23935
|
+
}
|
|
23936
|
+
const text = normalizeGoalText(args);
|
|
23937
|
+
goals.set(input.sessionID, {
|
|
23938
|
+
text,
|
|
23939
|
+
source: "manual",
|
|
23940
|
+
createdAt: Date.now()
|
|
23941
|
+
});
|
|
23942
|
+
pushText(output, `Set active goal:
|
|
23943
|
+
${text}`);
|
|
23944
|
+
},
|
|
23945
|
+
handleEvent: (input) => {
|
|
23946
|
+
const event = input.event;
|
|
23947
|
+
if (event.type === "session.created") {
|
|
23948
|
+
const info = event.properties?.info;
|
|
23949
|
+
if (!info?.id || !info.parentID)
|
|
23950
|
+
return;
|
|
23951
|
+
const parentGoal = goals.get(info.parentID);
|
|
23952
|
+
if (!parentGoal)
|
|
23953
|
+
return;
|
|
23954
|
+
goals.set(info.id, {
|
|
23955
|
+
inheritedFrom: info.parentID,
|
|
23956
|
+
createdAt: Date.now(),
|
|
23957
|
+
text: ""
|
|
23958
|
+
});
|
|
23959
|
+
return;
|
|
23960
|
+
}
|
|
23961
|
+
if (event.type === "session.deleted") {
|
|
23962
|
+
const props = event.properties;
|
|
23963
|
+
const sessionID = props?.info?.id ?? props?.sessionID;
|
|
23964
|
+
if (sessionID)
|
|
23965
|
+
goals.delete(sessionID);
|
|
23966
|
+
}
|
|
23967
|
+
},
|
|
23968
|
+
handleSystemTransform: (input, output) => {
|
|
23969
|
+
if (!input.sessionID)
|
|
23970
|
+
return;
|
|
23971
|
+
const resolved = resolveGoal(goals, input.sessionID);
|
|
23972
|
+
if (!resolved)
|
|
23973
|
+
return;
|
|
23974
|
+
const agentName = options?.getAgentName?.(input.sessionID);
|
|
23975
|
+
const { goal, inherited } = resolved;
|
|
23976
|
+
if (!inherited && agentName && agentName !== "orchestrator")
|
|
23977
|
+
return;
|
|
23978
|
+
const block = formatGoal(goal, inherited);
|
|
23979
|
+
if (output.system.some((entry) => entry.includes(block)))
|
|
23980
|
+
return;
|
|
23981
|
+
output.system.push(block);
|
|
23982
|
+
},
|
|
23983
|
+
getGoal: (sessionID) => resolveGoal(goals, sessionID)?.goal
|
|
23984
|
+
};
|
|
23985
|
+
}
|
|
23986
|
+
// src/hooks/image-hook.ts
|
|
23987
|
+
import { createHash } from "node:crypto";
|
|
23988
|
+
import {
|
|
23989
|
+
existsSync as existsSync6,
|
|
23990
|
+
mkdirSync as mkdirSync3,
|
|
23991
|
+
readdirSync as readdirSync2,
|
|
23992
|
+
rmdirSync,
|
|
23993
|
+
statSync as statSync3,
|
|
23994
|
+
unlinkSync as unlinkSync2,
|
|
23995
|
+
writeFileSync as writeFileSync3
|
|
23996
|
+
} from "node:fs";
|
|
23997
|
+
import { basename as basename3, extname as extname2, join as join8 } from "node:path";
|
|
23998
|
+
var lastCleanupByDir = new Map;
|
|
23999
|
+
var CLEANUP_INTERVAL = 10 * 60 * 1000;
|
|
24000
|
+
function isImagePart(p) {
|
|
24001
|
+
if (p.type === "image")
|
|
24002
|
+
return true;
|
|
24003
|
+
if (p.type === "file") {
|
|
24004
|
+
const mime = p.mime;
|
|
24005
|
+
if (mime?.startsWith("image/"))
|
|
24006
|
+
return true;
|
|
24007
|
+
const filename = p.filename;
|
|
24008
|
+
const name = p.name;
|
|
24009
|
+
const fileName = filename ?? name;
|
|
24010
|
+
if (fileName && /\.(png|jpg|jpeg|gif|bmp|webp|svg|ico|tiff?|heic)$/i.test(fileName))
|
|
24011
|
+
return true;
|
|
24012
|
+
}
|
|
24013
|
+
return false;
|
|
24014
|
+
}
|
|
24015
|
+
function decodeDataUrl(url) {
|
|
24016
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
24017
|
+
if (!match)
|
|
24018
|
+
return null;
|
|
24019
|
+
return { mime: match[1], data: Buffer.from(match[2], "base64") };
|
|
24020
|
+
}
|
|
24021
|
+
function extFromMime(mime) {
|
|
24022
|
+
const map = {
|
|
24023
|
+
"image/png": ".png",
|
|
24024
|
+
"image/jpeg": ".jpg",
|
|
24025
|
+
"image/gif": ".gif",
|
|
24026
|
+
"image/webp": ".webp",
|
|
24027
|
+
"image/svg+xml": ".svg",
|
|
24028
|
+
"image/bmp": ".bmp"
|
|
24029
|
+
};
|
|
24030
|
+
return map[mime] ?? ".png";
|
|
24031
|
+
}
|
|
24032
|
+
function sanitizeFilename(name) {
|
|
24033
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
24034
|
+
}
|
|
24035
|
+
function cleanupAllSessions(saveDir) {
|
|
24036
|
+
const now = Date.now();
|
|
24037
|
+
const lastCleanup = lastCleanupByDir.get(saveDir) ?? 0;
|
|
24038
|
+
if (now - lastCleanup < CLEANUP_INTERVAL)
|
|
24039
|
+
return;
|
|
24040
|
+
lastCleanupByDir.set(saveDir, now);
|
|
24041
|
+
const maxAge = 60 * 60 * 1000;
|
|
24042
|
+
const dirsToScan = [];
|
|
24043
|
+
try {
|
|
24044
|
+
for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
|
|
24045
|
+
const fp = join8(saveDir, entry.name);
|
|
24046
|
+
if (entry.isDirectory()) {
|
|
24047
|
+
dirsToScan.push(fp);
|
|
24048
|
+
} else {
|
|
24049
|
+
try {
|
|
24050
|
+
if (now - statSync3(fp).mtimeMs > maxAge)
|
|
24051
|
+
unlinkSync2(fp);
|
|
24052
|
+
} catch {}
|
|
24053
|
+
}
|
|
24054
|
+
}
|
|
24055
|
+
} catch {}
|
|
24056
|
+
for (const dir of dirsToScan) {
|
|
24057
|
+
try {
|
|
24058
|
+
let isEmpty = true;
|
|
24059
|
+
let allRemoved = true;
|
|
24060
|
+
for (const f of readdirSync2(dir)) {
|
|
24061
|
+
isEmpty = false;
|
|
24062
|
+
const fp = join8(dir, f);
|
|
24063
|
+
try {
|
|
24064
|
+
if (now - statSync3(fp).mtimeMs > maxAge) {
|
|
24065
|
+
unlinkSync2(fp);
|
|
24066
|
+
} else {
|
|
24067
|
+
allRemoved = false;
|
|
24068
|
+
}
|
|
24069
|
+
} catch {
|
|
24070
|
+
allRemoved = false;
|
|
24071
|
+
}
|
|
24072
|
+
}
|
|
24073
|
+
if (!isEmpty && allRemoved) {
|
|
24074
|
+
try {
|
|
24075
|
+
rmdirSync(dir);
|
|
24076
|
+
} catch {}
|
|
24077
|
+
}
|
|
24078
|
+
} catch {}
|
|
24079
|
+
}
|
|
24080
|
+
}
|
|
24081
|
+
function writeUniqueFile(dir, name, data, log2) {
|
|
24082
|
+
const ext = extname2(name);
|
|
24083
|
+
const base = basename3(name, ext) || name;
|
|
24084
|
+
let candidate = join8(dir, name);
|
|
24085
|
+
if (existsSync6(candidate)) {
|
|
24086
|
+
return candidate;
|
|
24087
|
+
}
|
|
24088
|
+
let counter = 0;
|
|
24089
|
+
const MAX_ATTEMPTS = 1000;
|
|
24090
|
+
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
24091
|
+
try {
|
|
24092
|
+
writeFileSync3(candidate, data, { flag: "wx" });
|
|
24093
|
+
return candidate;
|
|
24094
|
+
} catch (e) {
|
|
24095
|
+
if (e instanceof Error && e.code === "EEXIST") {
|
|
24096
|
+
counter += 1;
|
|
24097
|
+
candidate = join8(dir, `${base}-${counter}${ext}`);
|
|
24098
|
+
continue;
|
|
24099
|
+
}
|
|
24100
|
+
log2(`[image-hook] failed to save image: ${e}`);
|
|
24101
|
+
return null;
|
|
24102
|
+
}
|
|
24103
|
+
}
|
|
24104
|
+
log2(`[image-hook] failed to save image: max attempts (${MAX_ATTEMPTS}) reached`);
|
|
24105
|
+
return null;
|
|
24106
|
+
}
|
|
24107
|
+
function processImageAttachments(args) {
|
|
24108
|
+
const { messages, workDir, disabledAgents, log: log2 } = args;
|
|
24109
|
+
const observerEnabled = !disabledAgents.has("observer");
|
|
24110
|
+
if (!observerEnabled)
|
|
24111
|
+
return;
|
|
24112
|
+
const messagesWithImages = [];
|
|
24113
|
+
for (const msg of messages) {
|
|
24114
|
+
if (msg.info.role !== "user")
|
|
24115
|
+
continue;
|
|
24116
|
+
const imageParts = msg.parts.filter(isImagePart);
|
|
24117
|
+
if (imageParts.length > 0) {
|
|
24118
|
+
messagesWithImages.push({ msg, imageParts });
|
|
24119
|
+
}
|
|
24120
|
+
}
|
|
24121
|
+
const saveDir = join8(workDir, ".opencode", "images");
|
|
24122
|
+
if (messagesWithImages.length === 0) {
|
|
24123
|
+
if (existsSync6(saveDir))
|
|
24124
|
+
cleanupAllSessions(saveDir);
|
|
24125
|
+
return;
|
|
24126
|
+
}
|
|
24127
|
+
const gitignorePath = join8(workDir, ".opencode", ".gitignore");
|
|
24128
|
+
try {
|
|
24129
|
+
mkdirSync3(saveDir, { recursive: true });
|
|
24130
|
+
if (!existsSync6(gitignorePath))
|
|
24131
|
+
writeFileSync3(gitignorePath, `*
|
|
24132
|
+
`);
|
|
24133
|
+
} catch (e) {
|
|
24134
|
+
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
24135
|
+
}
|
|
24136
|
+
cleanupAllSessions(saveDir);
|
|
24137
|
+
for (const { msg, imageParts } of messagesWithImages) {
|
|
24138
|
+
const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
|
|
24139
|
+
const targetDir = sessionSubdir ? join8(saveDir, sessionSubdir) : saveDir;
|
|
24140
|
+
try {
|
|
24141
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
24142
|
+
} catch (e) {
|
|
24143
|
+
log2(`[image-hook] failed to create target image directory: ${e}`);
|
|
24144
|
+
}
|
|
24145
|
+
const savedPaths = [];
|
|
24146
|
+
for (const p of imageParts) {
|
|
24147
|
+
const url = p.url;
|
|
24148
|
+
const filename = p.filename ?? p.name;
|
|
24149
|
+
if (url) {
|
|
24150
|
+
const decoded = decodeDataUrl(url);
|
|
24151
|
+
if (decoded) {
|
|
24152
|
+
const hash = createHash("sha1").update(decoded.data).digest("hex").slice(0, 8);
|
|
24153
|
+
const sanitizedFilename = filename ? sanitizeFilename(filename) : undefined;
|
|
24154
|
+
const baseName = sanitizedFilename ? sanitizedFilename.replace(/\.[^.]+$/, "") || "image" : "image";
|
|
24155
|
+
const ext = sanitizedFilename ? extname2(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
|
|
24156
|
+
const name = `${baseName}-${hash}${ext}`;
|
|
24157
|
+
const filePath = writeUniqueFile(targetDir, name, decoded.data, log2);
|
|
24158
|
+
if (filePath)
|
|
24159
|
+
savedPaths.push(filePath);
|
|
24160
|
+
}
|
|
24161
|
+
}
|
|
24162
|
+
}
|
|
24163
|
+
const pathsText = savedPaths.length > 0 ? ` Saved to: ${savedPaths.join(", ")}` : "";
|
|
24164
|
+
log2(`[image-hook] stripping image/file parts, saving to disk${pathsText}`);
|
|
24165
|
+
msg.parts = msg.parts.filter((p) => !isImagePart(p)).concat([
|
|
24166
|
+
{
|
|
24167
|
+
type: "text",
|
|
24168
|
+
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.]`
|
|
24169
|
+
}
|
|
24170
|
+
]);
|
|
24171
|
+
}
|
|
24172
|
+
}
|
|
24173
|
+
// src/hooks/json-error-recovery/hook.ts
|
|
24174
|
+
var JSON_ERROR_TOOL_EXCLUDE_LIST = [
|
|
24175
|
+
"bash",
|
|
24176
|
+
"read",
|
|
24177
|
+
"glob",
|
|
24178
|
+
"webfetch",
|
|
24179
|
+
"grep_app_searchgithub",
|
|
24180
|
+
"websearch_web_search_exa"
|
|
24181
|
+
];
|
|
24182
|
+
var JSON_ERROR_PATTERNS = [
|
|
24183
|
+
/json parse error/i,
|
|
24184
|
+
/failed to parse json/i,
|
|
24185
|
+
/invalid json/i,
|
|
24186
|
+
/malformed json/i,
|
|
24187
|
+
/unexpected end of json input/i,
|
|
24188
|
+
/syntaxerror:\s*unexpected token.*json/i,
|
|
24189
|
+
/json[^\n]*expected '\}'/i,
|
|
24190
|
+
/json[^\n]*unexpected eof/i
|
|
24191
|
+
];
|
|
24192
|
+
var JSON_ERROR_REMINDER_MARKER = "[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]";
|
|
24193
|
+
var JSON_ERROR_EXCLUDED_TOOLS = new Set(JSON_ERROR_TOOL_EXCLUDE_LIST);
|
|
24194
|
+
var JSON_ERROR_REMINDER = `
|
|
24195
|
+
[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
|
|
24196
|
+
|
|
24197
|
+
You sent invalid JSON arguments. The system could not parse your tool call.
|
|
24198
|
+
STOP and do this NOW:
|
|
24199
|
+
|
|
24200
|
+
1. LOOK at the error message above to see what was expected vs what you sent.
|
|
24201
|
+
2. CORRECT your JSON syntax (missing braces, unescaped quotes, trailing commas, etc).
|
|
24202
|
+
3. RETRY the tool call with valid JSON.
|
|
24203
|
+
|
|
24204
|
+
DO NOT repeat the exact same invalid call.
|
|
24205
|
+
`;
|
|
24206
|
+
function createJsonErrorRecoveryHook(_ctx) {
|
|
24207
|
+
return {
|
|
24208
|
+
"tool.execute.after": async (input, output) => {
|
|
24209
|
+
if (JSON_ERROR_EXCLUDED_TOOLS.has(input.tool.toLowerCase()))
|
|
24210
|
+
return;
|
|
24211
|
+
if (typeof output.output !== "string")
|
|
24212
|
+
return;
|
|
24213
|
+
if (output.output.includes(JSON_ERROR_REMINDER_MARKER))
|
|
24214
|
+
return;
|
|
24215
|
+
const outputText = output.output;
|
|
24216
|
+
const hasJsonError = JSON_ERROR_PATTERNS.some((pattern) => pattern.test(outputText));
|
|
24217
|
+
if (hasJsonError) {
|
|
24218
|
+
output.output += `
|
|
24219
|
+
${JSON_ERROR_REMINDER}`;
|
|
24220
|
+
}
|
|
24221
|
+
}
|
|
24222
|
+
};
|
|
24223
|
+
}
|
|
24224
|
+
// src/hooks/phase-reminder/index.ts
|
|
24225
|
+
var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
|
|
24226
|
+
function createPhaseReminderHook() {
|
|
24227
|
+
return {
|
|
24228
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
24229
|
+
const { messages } = output;
|
|
24230
|
+
if (messages.length === 0) {
|
|
24231
|
+
return;
|
|
24232
|
+
}
|
|
24233
|
+
let lastUserMessageIndex = -1;
|
|
24234
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
24235
|
+
if (messages[i].info.role === "user") {
|
|
24236
|
+
lastUserMessageIndex = i;
|
|
24237
|
+
break;
|
|
24238
|
+
}
|
|
24239
|
+
}
|
|
24240
|
+
if (lastUserMessageIndex === -1) {
|
|
24241
|
+
return;
|
|
24242
|
+
}
|
|
24243
|
+
const lastUserMessage = messages[lastUserMessageIndex];
|
|
24244
|
+
const agent = lastUserMessage.info.agent;
|
|
24245
|
+
if (agent && agent !== "orchestrator") {
|
|
24196
24246
|
return;
|
|
24197
24247
|
}
|
|
24198
|
-
|
|
24199
|
-
|
|
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}`);
|
|
24248
|
+
const textPartIndex = lastUserMessage.parts.findIndex((p) => p.type === "text" && p.text !== undefined);
|
|
24249
|
+
if (textPartIndex === -1) {
|
|
24213
24250
|
return;
|
|
24214
24251
|
}
|
|
24215
|
-
const
|
|
24216
|
-
|
|
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
|
-
});
|
|
24252
|
+
const originalText = lastUserMessage.parts[textPartIndex].text ?? "";
|
|
24253
|
+
if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
|
|
24238
24254
|
return;
|
|
24239
24255
|
}
|
|
24240
|
-
if (
|
|
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)
|
|
24256
|
+
if (lastUserMessage.parts.some((p) => p.text?.includes(PHASE_REMINDER))) {
|
|
24252
24257
|
return;
|
|
24253
|
-
|
|
24254
|
-
|
|
24255
|
-
|
|
24258
|
+
}
|
|
24259
|
+
lastUserMessage.parts.push({
|
|
24260
|
+
type: "text",
|
|
24261
|
+
text: PHASE_REMINDER
|
|
24262
|
+
});
|
|
24263
|
+
}
|
|
24264
|
+
};
|
|
24265
|
+
}
|
|
24266
|
+
// src/hooks/post-file-tool-nudge/index.ts
|
|
24267
|
+
var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
|
|
24268
|
+
var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
|
|
24269
|
+
function createPostFileToolNudgeHook(options = {}) {
|
|
24270
|
+
function appendReminder(output) {
|
|
24271
|
+
if (typeof output.output !== "string") {
|
|
24272
|
+
return;
|
|
24273
|
+
}
|
|
24274
|
+
if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
|
|
24275
|
+
return;
|
|
24276
|
+
}
|
|
24277
|
+
output.output = [
|
|
24278
|
+
output.output,
|
|
24279
|
+
"",
|
|
24280
|
+
"<internal_reminder>",
|
|
24281
|
+
POST_FILE_TOOL_NUDGE,
|
|
24282
|
+
"</internal_reminder>"
|
|
24283
|
+
].join(`
|
|
24284
|
+
`);
|
|
24285
|
+
}
|
|
24286
|
+
return {
|
|
24287
|
+
"tool.execute.after": async (input, output) => {
|
|
24288
|
+
if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
|
|
24256
24289
|
return;
|
|
24257
|
-
|
|
24258
|
-
if (
|
|
24290
|
+
}
|
|
24291
|
+
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
24259
24292
|
return;
|
|
24260
|
-
|
|
24261
|
-
|
|
24262
|
-
|
|
24293
|
+
}
|
|
24294
|
+
appendReminder(output);
|
|
24295
|
+
}
|
|
24263
24296
|
};
|
|
24264
24297
|
}
|
|
24265
24298
|
// src/hooks/task-session-manager/index.ts
|
|
@@ -24842,7 +24875,7 @@ function createTodoHygiene(options) {
|
|
|
24842
24875
|
|
|
24843
24876
|
// src/hooks/todo-continuation/index.ts
|
|
24844
24877
|
var HOOK_NAME = "todo-continuation";
|
|
24845
|
-
var
|
|
24878
|
+
var COMMAND_NAME3 = "auto-continue";
|
|
24846
24879
|
var TODO_STATE_TIMEOUT_MS = 500;
|
|
24847
24880
|
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
24881
|
var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
|
|
@@ -25360,7 +25393,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
25360
25393
|
}
|
|
25361
25394
|
}
|
|
25362
25395
|
async function handleCommandExecuteBefore(input, output) {
|
|
25363
|
-
if (input.command !==
|
|
25396
|
+
if (input.command !== COMMAND_NAME3) {
|
|
25364
25397
|
return;
|
|
25365
25398
|
}
|
|
25366
25399
|
registerOrchestratorSession(input.sessionID);
|
|
@@ -25379,11 +25412,11 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
25379
25412
|
if (!newEnabled) {
|
|
25380
25413
|
cancelPendingTimer(state);
|
|
25381
25414
|
output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
|
|
25382
|
-
log(`[${HOOK_NAME}] Disabled via /${
|
|
25415
|
+
log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME3} command`);
|
|
25383
25416
|
return;
|
|
25384
25417
|
}
|
|
25385
25418
|
state.suppressUntil = 0;
|
|
25386
|
-
log(`[${HOOK_NAME}] Enabled via /${
|
|
25419
|
+
log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME3} command`, {
|
|
25387
25420
|
maxContinuations
|
|
25388
25421
|
});
|
|
25389
25422
|
let hasIncompleteTodos = false;
|
|
@@ -28229,7 +28262,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
|
28229
28262
|
}
|
|
28230
28263
|
|
|
28231
28264
|
// src/interview/service.ts
|
|
28232
|
-
var
|
|
28265
|
+
var COMMAND_NAME4 = "interview";
|
|
28233
28266
|
var DEFAULT_MAX_QUESTIONS = 2;
|
|
28234
28267
|
function isTruthyEnvFlag(value) {
|
|
28235
28268
|
if (!value) {
|
|
@@ -28476,11 +28509,11 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28476
28509
|
}
|
|
28477
28510
|
function registerCommand(opencodeConfig) {
|
|
28478
28511
|
const configCommand = opencodeConfig.command;
|
|
28479
|
-
if (!configCommand?.[
|
|
28512
|
+
if (!configCommand?.[COMMAND_NAME4]) {
|
|
28480
28513
|
if (!opencodeConfig.command) {
|
|
28481
28514
|
opencodeConfig.command = {};
|
|
28482
28515
|
}
|
|
28483
|
-
opencodeConfig.command[
|
|
28516
|
+
opencodeConfig.command[COMMAND_NAME4] = {
|
|
28484
28517
|
template: "Start an interview and write a live markdown spec",
|
|
28485
28518
|
description: "Open a localhost interview UI linked to the current OpenCode session"
|
|
28486
28519
|
};
|
|
@@ -28554,7 +28587,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28554
28587
|
}
|
|
28555
28588
|
}
|
|
28556
28589
|
async function handleCommandExecuteBefore(input, output) {
|
|
28557
|
-
if (input.command !==
|
|
28590
|
+
if (input.command !== COMMAND_NAME4) {
|
|
28558
28591
|
return;
|
|
28559
28592
|
}
|
|
28560
28593
|
const idea = input.arguments.trim();
|
|
@@ -30808,11 +30841,11 @@ function recordTuiAgentModel(input) {
|
|
|
30808
30841
|
}
|
|
30809
30842
|
|
|
30810
30843
|
// src/tools/preset-manager.ts
|
|
30811
|
-
var
|
|
30844
|
+
var COMMAND_NAME5 = "preset";
|
|
30812
30845
|
function createPresetManager(ctx, config) {
|
|
30813
30846
|
let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
|
|
30814
30847
|
async function handleCommandExecuteBefore(input, output) {
|
|
30815
|
-
if (input.command !==
|
|
30848
|
+
if (input.command !== COMMAND_NAME5) {
|
|
30816
30849
|
return;
|
|
30817
30850
|
}
|
|
30818
30851
|
output.parts.length = 0;
|
|
@@ -30831,11 +30864,11 @@ function createPresetManager(ctx, config) {
|
|
|
30831
30864
|
}
|
|
30832
30865
|
function registerCommand(opencodeConfig) {
|
|
30833
30866
|
const configCommand = opencodeConfig.command;
|
|
30834
|
-
if (!configCommand?.[
|
|
30867
|
+
if (!configCommand?.[COMMAND_NAME5]) {
|
|
30835
30868
|
if (!opencodeConfig.command) {
|
|
30836
30869
|
opencodeConfig.command = {};
|
|
30837
30870
|
}
|
|
30838
|
-
opencodeConfig.command[
|
|
30871
|
+
opencodeConfig.command[COMMAND_NAME5] = {
|
|
30839
30872
|
template: "List available presets and switch between them",
|
|
30840
30873
|
description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
|
|
30841
30874
|
};
|
|
@@ -33269,460 +33302,6 @@ function createWebfetchTool(pluginCtx, options = {}) {
|
|
|
33269
33302
|
}
|
|
33270
33303
|
});
|
|
33271
33304
|
}
|
|
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
33305
|
// src/utils/subagent-depth.ts
|
|
33727
33306
|
class SubagentDepthTracker {
|
|
33728
33307
|
depthBySession = new Map;
|
|
@@ -33835,7 +33414,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33835
33414
|
let jsonErrorRecoveryHook;
|
|
33836
33415
|
let foregroundFallback;
|
|
33837
33416
|
let todoContinuationHook;
|
|
33838
|
-
let
|
|
33417
|
+
let deepworkCommandHook;
|
|
33418
|
+
let goalHook;
|
|
33839
33419
|
let taskSessionManagerHook;
|
|
33840
33420
|
let backgroundJobBoard;
|
|
33841
33421
|
let interviewManager;
|
|
@@ -33844,8 +33424,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33844
33424
|
let councilTools;
|
|
33845
33425
|
let webfetch;
|
|
33846
33426
|
let rewriteDisplayNameMentions;
|
|
33847
|
-
let subtaskCommandManager;
|
|
33848
|
-
let subtaskState;
|
|
33849
33427
|
let toolCount = 0;
|
|
33850
33428
|
try {
|
|
33851
33429
|
config = loadPluginConfig(ctx.directory);
|
|
@@ -33931,7 +33509,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33931
33509
|
autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
|
|
33932
33510
|
backgroundJobBoard
|
|
33933
33511
|
});
|
|
33934
|
-
|
|
33512
|
+
deepworkCommandHook = createDeepworkCommandHook();
|
|
33513
|
+
goalHook = createGoalHook(ctx, config, {
|
|
33935
33514
|
getAgentName: (sessionID) => sessionAgentMap.get(sessionID)
|
|
33936
33515
|
});
|
|
33937
33516
|
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
@@ -33944,9 +33523,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33944
33523
|
interviewManager = createInterviewManager(ctx, config);
|
|
33945
33524
|
presetManager = createPresetManager(ctx, config);
|
|
33946
33525
|
divoomManager = createDivoomManager(config.divoom);
|
|
33947
|
-
|
|
33948
|
-
subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
|
|
33949
|
-
toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2 + 2;
|
|
33526
|
+
toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
|
|
33950
33527
|
} catch (err) {
|
|
33951
33528
|
log("[plugin] FATAL: init failed", String(err));
|
|
33952
33529
|
await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
|
|
@@ -33991,9 +33568,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33991
33568
|
webfetch,
|
|
33992
33569
|
...todoContinuationHook.tool,
|
|
33993
33570
|
ast_grep_search,
|
|
33994
|
-
ast_grep_replace
|
|
33995
|
-
subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
|
|
33996
|
-
read_session: createReadSessionTool(ctx.client, subtaskState)
|
|
33571
|
+
ast_grep_replace
|
|
33997
33572
|
},
|
|
33998
33573
|
mcp: mcps,
|
|
33999
33574
|
config: async (opencodeConfig) => {
|
|
@@ -34189,9 +33764,9 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34189
33764
|
};
|
|
34190
33765
|
}
|
|
34191
33766
|
interviewManager.registerCommand(opencodeConfig);
|
|
34192
|
-
|
|
33767
|
+
goalHook.registerCommand(opencodeConfig);
|
|
33768
|
+
deepworkCommandHook.registerCommand(opencodeConfig);
|
|
34193
33769
|
presetManager.registerCommand(opencodeConfig);
|
|
34194
|
-
subtaskCommandManager.registerCommand(opencodeConfig);
|
|
34195
33770
|
},
|
|
34196
33771
|
event: async (input) => {
|
|
34197
33772
|
const event = input.event;
|
|
@@ -34216,11 +33791,10 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34216
33791
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
34217
33792
|
await foregroundFallback.handleEvent(input.event);
|
|
34218
33793
|
await todoContinuationHook.handleEvent(input);
|
|
34219
|
-
|
|
33794
|
+
goalHook.handleEvent(input);
|
|
34220
33795
|
await autoUpdateChecker.event(input);
|
|
34221
33796
|
await interviewManager.handleEvent(input);
|
|
34222
33797
|
await taskSessionManagerHook.event(input);
|
|
34223
|
-
subtaskCommandManager.handleEvent(input);
|
|
34224
33798
|
if (event.type === "permission.asked" || event.type === "question.asked") {
|
|
34225
33799
|
const props = event.properties;
|
|
34226
33800
|
divoomManager.onUserInputRequired({
|
|
@@ -34278,7 +33852,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34278
33852
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
34279
33853
|
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
34280
33854
|
await presetManager.handleCommandExecuteBefore(input, output);
|
|
34281
|
-
await
|
|
33855
|
+
await goalHook.handleCommandExecuteBefore(input, output);
|
|
33856
|
+
await deepworkCommandHook.handleCommandExecuteBefore(input, output);
|
|
34282
33857
|
},
|
|
34283
33858
|
"chat.headers": chatHeadersHook["chat.headers"],
|
|
34284
33859
|
"chat.message": async (input, output) => {
|
|
@@ -34307,7 +33882,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34307
33882
|
${output.system[0]}` : "");
|
|
34308
33883
|
}
|
|
34309
33884
|
}
|
|
34310
|
-
|
|
33885
|
+
goalHook.handleSystemTransform(input, output);
|
|
34311
33886
|
collapseSystemInPlace(output.system);
|
|
34312
33887
|
},
|
|
34313
33888
|
"experimental.chat.messages.transform": async (input, output) => {
|