opencode-orchestrator 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -13,6 +13,24 @@
13
13
  import type { PluginInput } from "@opencode-ai/plugin";
14
14
  declare const OrchestratorPlugin: (input: PluginInput) => Promise<{
15
15
  tool: {
16
+ git_branch: {
17
+ description: string;
18
+ args: {
19
+ action: import("zod").ZodEnum<{
20
+ status: "status";
21
+ diff: "diff";
22
+ current: "current";
23
+ list: "list";
24
+ recent: "recent";
25
+ all: "all";
26
+ }>;
27
+ baseBranch: import("zod").ZodOptional<import("zod").ZodString>;
28
+ };
29
+ execute(args: {
30
+ action: "status" | "diff" | "current" | "list" | "recent" | "all";
31
+ baseBranch?: string | undefined;
32
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
33
+ };
16
34
  call_agent: {
17
35
  description: string;
18
36
  args: {
@@ -108,13 +126,13 @@ declare const OrchestratorPlugin: (input: PluginInput) => Promise<{
108
126
  args: {
109
127
  status: import("zod").ZodOptional<import("zod").ZodEnum<{
110
128
  running: "running";
129
+ all: "all";
111
130
  done: "done";
112
131
  error: "error";
113
- all: "all";
114
132
  }>>;
115
133
  };
116
134
  execute(args: {
117
- status?: "running" | "done" | "error" | "all" | undefined;
135
+ status?: "running" | "all" | "done" | "error" | undefined;
118
136
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
119
137
  };
120
138
  kill_background: {
@@ -139,7 +157,8 @@ declare const OrchestratorPlugin: (input: PluginInput) => Promise<{
139
157
  output: string;
140
158
  metadata: any;
141
159
  }) => Promise<void>;
142
- event: (input: {
160
+ "assistant.done": (assistantInput: any, assistantOutput: any) => Promise<void>;
161
+ handler: ({ event }: {
143
162
  event: {
144
163
  type: string;
145
164
  properties?: unknown;
package/dist/index.js CHANGED
@@ -13798,6 +13798,206 @@ Returns matches grouped by pattern, with file paths and line numbers.
13798
13798
  }
13799
13799
  });
13800
13800
 
13801
+ // src/tools/git.ts
13802
+ var gitBranchTool = (directory) => tool({
13803
+ description: `Get Git branch information and status.
13804
+
13805
+ <purpose>
13806
+ Analyze current Git workspace context including:
13807
+ - Current branch name
13808
+ - All branches (local and remote)
13809
+ - Branch relationships (upstream, ahead/behind)
13810
+ - Staged, unstaged, and untracked files
13811
+ - Recent commits
13812
+ </purpose>
13813
+
13814
+ <examples>
13815
+ - Get current branch: Returns branch name and status
13816
+ - List all branches: Shows local and remote branches
13817
+ - Check status: Shows modified files
13818
+ - Recent commits: Last 5 commits with file changes
13819
+ </examples>
13820
+
13821
+ <output>
13822
+ Returns structured Git information for better code decisions.
13823
+ </output>`,
13824
+ args: {
13825
+ action: tool.schema.enum([
13826
+ "current",
13827
+ "list",
13828
+ "status",
13829
+ "diff",
13830
+ "recent",
13831
+ "all"
13832
+ ]).describe("Action to perform: current, list, status, diff, recent, all"),
13833
+ baseBranch: tool.schema.string().optional().describe("Base branch for comparison (e.g., 'main', 'develop'). Required for diff action.")
13834
+ },
13835
+ async execute(args) {
13836
+ const { action, baseBranch } = args;
13837
+ try {
13838
+ switch (action) {
13839
+ case "current":
13840
+ return await getCurrentBranch(directory);
13841
+ case "list":
13842
+ return await listBranches(directory);
13843
+ case "status":
13844
+ return await getGitStatus(directory);
13845
+ case "diff":
13846
+ return await getDiff(directory, baseBranch);
13847
+ case "recent":
13848
+ return await getRecentCommits(directory, 5);
13849
+ case "all":
13850
+ return await getAllInfo(directory);
13851
+ default:
13852
+ return await getCurrentBranch(directory);
13853
+ }
13854
+ } catch (error45) {
13855
+ return "\u274C Git error: " + (error45 instanceof Error ? error45.message : String(error45));
13856
+ }
13857
+ }
13858
+ });
13859
+ async function execGit(directory, args) {
13860
+ const { execSync } = await import("child_process");
13861
+ try {
13862
+ return execSync("git " + args.join(" "), {
13863
+ cwd: directory,
13864
+ encoding: "utf-8",
13865
+ stdio: ["ignore", "pipe", "pipe"],
13866
+ maxBuffer: 10 * 1024 * 1024
13867
+ // 10MB
13868
+ }).toString();
13869
+ } catch (error45) {
13870
+ if (error45.status === 128) {
13871
+ throw new Error("Not a git repository");
13872
+ }
13873
+ throw error45;
13874
+ }
13875
+ }
13876
+ async function getAheadBehind(directory, branch) {
13877
+ try {
13878
+ const output = await execGit(directory, ["rev-list", "--left-right", "--count", branch + "...@{u}"]);
13879
+ const parts = output.trim().split(/\s+/);
13880
+ const ahead = parseInt(parts[0], 10);
13881
+ const behind = parseInt(parts[1] || "0", 10);
13882
+ const resultParts = [];
13883
+ if (ahead > 0) resultParts.push(ahead + " ahead");
13884
+ if (behind > 0) resultParts.push(behind + " behind");
13885
+ return resultParts.length > 0 ? "| **Sync** | " + resultParts.join(", ") + " |" : "";
13886
+ } catch {
13887
+ return "";
13888
+ }
13889
+ }
13890
+ async function getCurrentBranch(directory) {
13891
+ const output = await execGit(directory, ["branch", "--show-current"]);
13892
+ const current = output.trim() || "HEAD (detached)";
13893
+ const upstream = await execGit(directory, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).catch(() => "");
13894
+ return "\u{1F33F} **Current Branch**: `" + current + "`\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" + (upstream ? "| **Upstream** | `" + upstream + "` |" : "") + await getAheadBehind(directory, current);
13895
+ }
13896
+ async function listBranches(directory) {
13897
+ const branches = await execGit(directory, ["branch", "-vv"]);
13898
+ const branchList = branches.split("\n").filter((b) => b.trim()).map((b) => {
13899
+ const isCurrent = b.startsWith("*");
13900
+ const name = isCurrent ? b.substring(2).trim() : b.trim();
13901
+ const parts = name.split(/\s+/);
13902
+ const branchName = parts[0];
13903
+ const upstream = parts[1] ? parts[1].match(/\[([^\]]+)\]/)?.[1] : void 0;
13904
+ const icon = isCurrent ? "\u{1F33F}" : "\u{1F4C2}";
13905
+ const status = isCurrent ? "(current)" : "";
13906
+ const upstreamInfo = upstream ? "\u2192 " + upstream : "";
13907
+ return icon + " `" + branchName + "` " + status + " " + upstreamInfo;
13908
+ }).join("\n");
13909
+ return "\u{1F33F} **All Branches**\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" + branchList;
13910
+ }
13911
+ async function getGitStatus(directory) {
13912
+ const status = await execGit(directory, ["status", "--porcelain"]);
13913
+ const lines = status.split("\n").filter((l) => l.trim());
13914
+ if (lines.length === 0) {
13915
+ return "\u2705 Working directory clean";
13916
+ }
13917
+ const staged = [];
13918
+ const unstaged = [];
13919
+ const untracked = [];
13920
+ const conflicted = [];
13921
+ for (const line of lines) {
13922
+ if (!line || line.length < 2) continue;
13923
+ const statusCode = line.substring(0, 2);
13924
+ const filename = line.substring(3);
13925
+ if (statusCode === "??") {
13926
+ untracked.push(filename);
13927
+ } else if (statusCode.startsWith("U") || statusCode === "AA") {
13928
+ conflicted.push(filename);
13929
+ } else if (statusCode[0] !== " ") {
13930
+ staged.push(filename);
13931
+ }
13932
+ if (statusCode[1] !== " " && statusCode[1] !== "?") {
13933
+ unstaged.push(filename);
13934
+ }
13935
+ }
13936
+ const current = await execGit(directory, ["branch", "--show-current"]).catch(() => "unknown");
13937
+ let result = "\u{1F33F} **Branch**: `" + current + "`\n\n";
13938
+ if (staged.length > 0) {
13939
+ result += "\u2705 **Staged** (" + staged.length + ")\n" + staged.map((f) => " + " + f).join("\n") + "\n\n";
13940
+ }
13941
+ if (unstaged.length > 0) {
13942
+ result += "\u{1F4DD} **Modified** (" + unstaged.length + ")\n" + unstaged.map((f) => " ~ " + f).join("\n") + "\n\n";
13943
+ }
13944
+ if (untracked.length > 0) {
13945
+ result += "\u2795 **Untracked** (" + untracked.length + ")\n" + untracked.slice(0, 10).map((f) => " ? " + f).join("\n") + (untracked.length > 10 ? "\n ... and " + (untracked.length - 10) + " more" : "") + "\n\n";
13946
+ }
13947
+ if (conflicted.length > 0) {
13948
+ result += "\u26A0\uFE0F **Conflicts** (" + conflicted.length + ")\n" + conflicted.map((f) => " ! " + f).join("\n") + "\n\n";
13949
+ }
13950
+ return result.trim();
13951
+ }
13952
+ async function getDiff(directory, baseBranch) {
13953
+ const current = await execGit(directory, ["branch", "--show-current"]).catch(() => "HEAD");
13954
+ const base = baseBranch || "main";
13955
+ const diff = await execGit(directory, ["diff", base + "..." + current, "--stat"]);
13956
+ if (!diff.trim()) {
13957
+ return "\u2705 No differences between `" + current + "` and `" + base + "`";
13958
+ }
13959
+ const files = await execGit(directory, ["diff", base + "..." + current, "--name-only"]);
13960
+ const fileList = files.split("\n").filter((f) => f.trim());
13961
+ return "\u{1F4CA} **Diff**: `" + current + "` vs `" + base + "`\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" + diff + "\n\n\u{1F4C1} **Changed Files** (" + fileList.length + "):\n" + fileList.map((f, i) => " " + (i + 1) + ". " + f).join("\n");
13962
+ }
13963
+ async function getRecentCommits(directory, count = 5) {
13964
+ const log2 = await execGit(directory, [
13965
+ "log",
13966
+ "-" + count,
13967
+ "--pretty=format:%H|%an|%ad|%s",
13968
+ "--date=short",
13969
+ "--name-only"
13970
+ ]);
13971
+ const blocks = log2.split("\n\n").filter((b) => b.trim());
13972
+ const commits = [];
13973
+ for (const block of blocks) {
13974
+ const lines = block.split("\n").filter((l) => l.trim());
13975
+ if (lines.length < 2) continue;
13976
+ const [hash2, author, date5, message] = lines[0].split("|");
13977
+ const files = lines.slice(1);
13978
+ commits.push({
13979
+ hash: hash2.substring(0, 7),
13980
+ author,
13981
+ date: date5,
13982
+ message,
13983
+ files
13984
+ });
13985
+ }
13986
+ let result = "\u{1F4DC} **Recent Commits** (last " + commits.length + ")\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n";
13987
+ for (const commit of commits) {
13988
+ result += "\n\u{1F539} `" + commit.hash + "` - " + commit.message + "\n";
13989
+ result += " \u{1F464} " + commit.author + " | \u{1F4C5} " + commit.date + "\n";
13990
+ result += " \u{1F4C1} " + commit.files.length + " file" + (commit.files.length > 1 ? "s" : "") + "\n";
13991
+ }
13992
+ return result;
13993
+ }
13994
+ async function getAllInfo(directory) {
13995
+ const current = await getCurrentBranch(directory);
13996
+ const status = await getGitStatus(directory);
13997
+ const recent = await getRecentCommits(directory, 3);
13998
+ return "\u{1F50D} **Full Git Context**\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n" + current + "\n\n" + status + "\n\n" + recent;
13999
+ }
14000
+
13801
14001
  // src/core/background.ts
13802
14002
  import { spawn as spawn2 } from "child_process";
13803
14003
  import { randomBytes } from "crypto";
@@ -15474,6 +15674,476 @@ function createAsyncAgentTools(manager, client) {
15474
15674
  };
15475
15675
  }
15476
15676
 
15677
+ // src/core/batch-processor.ts
15678
+ var SmartBatchProcessor = class {
15679
+ parallelAgentManager;
15680
+ tasks = /* @__PURE__ */ new Map();
15681
+ constructor(parallelAgentManager2) {
15682
+ this.parallelAgentManager = parallelAgentManager2;
15683
+ }
15684
+ /**
15685
+ * Process a batch of tasks with smart validation
15686
+ */
15687
+ async processBatch(tasks, options = {
15688
+ concurrency: 3,
15689
+ maxRetries: 2,
15690
+ validateAfterEach: false,
15691
+ continueOnError: true
15692
+ }) {
15693
+ const startTime = Date.now();
15694
+ for (const task of tasks) {
15695
+ this.tasks.set(task.id, {
15696
+ ...task,
15697
+ status: "pending",
15698
+ attempts: 0
15699
+ });
15700
+ }
15701
+ console.log(`
15702
+ \u{1F4E6} [Smart Batch] Starting ${tasks.length} tasks with concurrency ${options.concurrency}`);
15703
+ console.log(` Strategy: ${options.validateAfterEach ? "Validate each" : "Centralized validation"}`);
15704
+ await this.executePhase(tasks, options, "initial");
15705
+ const failedTasks = Array.from(this.tasks.values()).filter((t) => t.status === "failed" || t.status === "pending");
15706
+ if (failedTasks.length === 0) {
15707
+ console.log(`
15708
+ \u2705 [Smart Batch] All ${tasks.length} tasks succeeded!`);
15709
+ return this.buildResult(startTime, tasks);
15710
+ }
15711
+ console.log(`
15712
+ \u{1F50D} [Smart Batch] Validation complete: ${failedTasks.length}/${tasks.length} tasks need retry`);
15713
+ let retryCount = 0;
15714
+ while (failedTasks.length > 0 && retryCount < options.maxRetries) {
15715
+ retryCount++;
15716
+ console.log(`
15717
+ \u{1F504} [Smart Batch] Retry round ${retryCount}/${options.maxRetries} for ${failedTasks.length} tasks`);
15718
+ await this.executePhase(failedTasks, options, `retry-${retryCount}`);
15719
+ const stillFailed = Array.from(this.tasks.values()).filter((t) => t.status === "failed");
15720
+ if (stillFailed.length === failedTasks.length) {
15721
+ console.log(`\u26A0\uFE0F [Smart Batch] No progress in retry round ${retryCount}, stopping`);
15722
+ break;
15723
+ }
15724
+ }
15725
+ return this.buildResult(startTime, tasks);
15726
+ }
15727
+ /**
15728
+ * Execute a phase with concurrency control
15729
+ */
15730
+ async executePhase(tasks, options, phase) {
15731
+ for (const task of tasks) {
15732
+ this.parallelAgentManager.setConcurrencyLimit(task.agent, options.concurrency);
15733
+ }
15734
+ const batches = [];
15735
+ for (let i = 0; i < tasks.length; i += options.concurrency) {
15736
+ batches.push(tasks.slice(i, i + options.concurrency));
15737
+ }
15738
+ for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
15739
+ const batch = batches[batchIndex];
15740
+ console.log(` [${phase}] Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} tasks)`);
15741
+ const batchPromises = batch.map((task) => this.executeTask(task, options));
15742
+ await Promise.all(batchPromises);
15743
+ if (!options.validateAfterEach) {
15744
+ continue;
15745
+ }
15746
+ const failedInBatch = batch.filter((t) => t.status === "failed");
15747
+ if (failedInBatch.length > 0) {
15748
+ console.log(` [${phase}] ${failedInBatch.length} failed in batch, immediate retry`);
15749
+ const retryPromises = failedInBatch.map((t) => this.executeTask(t, options));
15750
+ await Promise.all(retryPromises);
15751
+ }
15752
+ }
15753
+ }
15754
+ /**
15755
+ * Execute a single task
15756
+ */
15757
+ async executeTask(task, options) {
15758
+ const currentTask = this.tasks.get(task.id);
15759
+ if (!options.continueOnError && currentTask.status === "failed") {
15760
+ return;
15761
+ }
15762
+ try {
15763
+ currentTask.attempts++;
15764
+ currentTask.status = "pending";
15765
+ const launched = await this.parallelAgentManager.launch({
15766
+ agent: task.agent,
15767
+ description: task.description,
15768
+ prompt: task.prompt,
15769
+ parentSessionID: ""
15770
+ // Would need actual parent ID
15771
+ });
15772
+ const maxWaitTime = 5 * 60 * 1e3;
15773
+ const startTime = Date.now();
15774
+ while (Date.now() - startTime < maxWaitTime) {
15775
+ const taskData = this.parallelAgentManager.getTask(launched.id);
15776
+ if (!taskData) break;
15777
+ if (taskData.status === "completed") {
15778
+ currentTask.status = "success";
15779
+ console.log(` \u2705 [${task.id}] Success on attempt ${currentTask.attempts}`);
15780
+ return;
15781
+ }
15782
+ if (taskData.status === "error" || taskData.status === "timeout") {
15783
+ currentTask.status = "failed";
15784
+ currentTask.error = taskData.error || "Unknown error";
15785
+ console.log(` \u274C [${task.id}] Failed: ${currentTask.error}`);
15786
+ return;
15787
+ }
15788
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
15789
+ }
15790
+ currentTask.status = "failed";
15791
+ currentTask.error = "Timeout";
15792
+ } catch (error45) {
15793
+ currentTask.status = "failed";
15794
+ currentTask.error = error45 instanceof Error ? error45.message : String(error45);
15795
+ console.log(` \u274C [${task.id}] Exception: ${currentTask.error}`);
15796
+ }
15797
+ }
15798
+ /**
15799
+ * Build result summary
15800
+ */
15801
+ buildResult(startTime, tasks) {
15802
+ const allTasks = Array.from(this.tasks.values());
15803
+ const successCount = allTasks.filter((t) => t.status === "success").length;
15804
+ const failedCount = allTasks.filter((t) => t.status === "failed").length;
15805
+ const retriedCount = allTasks.filter((t) => t.attempts > 1).length;
15806
+ return {
15807
+ total: tasks.length,
15808
+ success: successCount,
15809
+ failed: failedCount,
15810
+ retried: retriedCount,
15811
+ duration: Date.now() - startTime,
15812
+ tasks: allTasks
15813
+ };
15814
+ }
15815
+ /**
15816
+ * Export failed tasks for manual review
15817
+ */
15818
+ exportFailedTasks() {
15819
+ const failedTasks = Array.from(this.tasks.values()).filter((t) => t.status === "failed");
15820
+ if (failedTasks.length === 0) {
15821
+ return "\u2705 No failed tasks to export.";
15822
+ }
15823
+ const output = failedTasks.map((task) => {
15824
+ return `
15825
+ ---
15826
+ Task ID: ${task.id}
15827
+ Agent: ${task.agent}
15828
+ Attempts: ${task.attempts}
15829
+ Error: ${task.error}
15830
+ Description: ${task.description}
15831
+ ---
15832
+ `.trim();
15833
+ }).join("\n");
15834
+ return `\u274C ${failedTasks.length} failed tasks:
15835
+ ${output}`;
15836
+ }
15837
+ /**
15838
+ * Clear all tasks
15839
+ */
15840
+ clear() {
15841
+ this.tasks.clear();
15842
+ }
15843
+ };
15844
+
15845
+ // src/tools/batch.ts
15846
+ var createProcessBatchTool = (parallelAgentManager2, client) => tool({
15847
+ description: `Execute a batch of tasks with intelligent validation and retry.
15848
+
15849
+ <strategy>
15850
+ 1. Execute all tasks in parallel (respecting concurrency limits)
15851
+ 2. Centralized validation: Identify ALL failed tasks at once
15852
+ 3. Retry ONLY failed tasks (not everything)
15853
+ </strategy>
15854
+
15855
+ <benefits>
15856
+ - Faster than naive retry: Failed tasks batch-identified
15857
+ - More efficient: No redundant work on successful tasks
15858
+ - Controlled: Concurrency limits prevent API overload
15859
+ </benefits>
15860
+
15861
+ <examples>
15862
+ process_batch({
15863
+ concurrency: 10,
15864
+ tasks: [
15865
+ { id: "task1", agent: "builder", description: "Test A", prompt: "..." },
15866
+ { id: "task2", agent: "inspector", description: "Test B", prompt: "..." }
15867
+ ]
15868
+ })
15869
+ </examples>`,
15870
+ args: {
15871
+ concurrency: tool.schema.string().describe("Concurrency limit (default: 3, max: 10)"),
15872
+ maxRetries: tool.schema.string().optional().describe("Maximum retry rounds (default: 2)"),
15873
+ validateAfterEach: tool.schema.boolean().optional().describe("Validate after each task (default: false = centralized validation)"),
15874
+ tasks: tool.schema.string().describe("Array of task objects in JSON format")
15875
+ },
15876
+ async execute(args) {
15877
+ const { concurrency, maxRetries = "2", validateAfterEach = false, tasks } = args;
15878
+ let taskList;
15879
+ try {
15880
+ taskList = JSON.parse(tasks);
15881
+ } catch {
15882
+ return "\u274C Invalid tasks JSON. Must be valid array.";
15883
+ }
15884
+ if (!Array.isArray(taskList)) {
15885
+ return "\u274C tasks must be an array of task objects.";
15886
+ }
15887
+ for (const task of taskList) {
15888
+ if (!task.id || !task.agent || !task.description || !task.prompt) {
15889
+ return `\u274C Task missing required fields (id, agent, description, prompt)`;
15890
+ }
15891
+ }
15892
+ const numConcurrency = parseInt(concurrency, 10);
15893
+ const numRetries = parseInt(maxRetries, 10);
15894
+ if (isNaN(numConcurrency) || numConcurrency < 1 || numConcurrency > 10) {
15895
+ return "\u274C Invalid concurrency. Must be 1-10.";
15896
+ }
15897
+ if (isNaN(numRetries) || numRetries < 0 || numRetries > 5) {
15898
+ return "\u274C Invalid maxRetries. Must be 0-5.";
15899
+ }
15900
+ const processor = new SmartBatchProcessor(parallelAgentManager2);
15901
+ const result = await processor.processBatch(taskList, {
15902
+ concurrency: numConcurrency,
15903
+ maxRetries: numRetries,
15904
+ validateAfterEach,
15905
+ continueOnError: true
15906
+ });
15907
+ const durationSecs = Math.floor(result.duration / 1e3);
15908
+ const successRate = (result.success / result.total * 100).toFixed(1);
15909
+ let output = `\u2705 **Batch Processing Complete**
15910
+
15911
+ | Metric | Value |
15912
+ |---------|--------|
15913
+ | **Total Tasks** | ${result.total} |
15914
+ | **Successful** | ${result.success} (${successRate}%) |
15915
+ | **Failed** | ${result.failed} |
15916
+ | **Retried** | ${result.retried} |
15917
+ | **Duration** | ${durationSecs}s |
15918
+
15919
+ `;
15920
+ if (result.failed > 0) {
15921
+ output += `\u26A0\uFE0F **Failed Tasks**
15922
+
15923
+ Use \`export_failed_tasks()\` to review failed tasks and manually fix issues.
15924
+
15925
+ ---
15926
+
15927
+ Failed Task IDs:
15928
+ ${result.tasks.filter((t) => t.status === "failed").map((t) => ` - ${t.id}`).join("\n")}
15929
+ ---
15930
+ `;
15931
+ }
15932
+ return output;
15933
+ }
15934
+ });
15935
+ var createExportFailedTasksTool = (parallelAgentManager2) => tool({
15936
+ description: `Export failed tasks from the last batch for manual review.`,
15937
+ args: {},
15938
+ async execute() {
15939
+ const processor = new SmartBatchProcessor(parallelAgentManager2);
15940
+ return processor.exportFailedTasks();
15941
+ }
15942
+ });
15943
+ var createCompareStrategiesTool = (parallelAgentManager2) => tool({
15944
+ description: `Compare naive retry vs smart batch validation performance.
15945
+
15946
+ <comparison>
15947
+ **Naive Strategy** (current):
15948
+ - Concurrency: 3 fixed
15949
+ - Retry: Per-task immediate retry
15950
+ - Issue: Slow for large batches, redundant work
15951
+
15952
+ **Smart Batch Strategy** (new):
15953
+ - Concurrency: Configurable (up to 10)
15954
+ - Validation: Centralized, batch-identify failures
15955
+ - Retry: Only failed tasks
15956
+ - Benefit: Faster, less redundant work
15957
+ </comparison>`,
15958
+ args: {
15959
+ taskCount: tool.schema.string().describe("Number of simulated tasks (default: 100)"),
15960
+ concurrency1: tool.schema.string().optional().describe("Strategy 1 concurrency (default: 3)"),
15961
+ concurrency2: tool.schema.string().optional().describe("Strategy 2 concurrency (default: 10)")
15962
+ },
15963
+ async execute(args) {
15964
+ const taskCount = parseInt(args.taskCount || "100", 10);
15965
+ const concurrency1 = parseInt(args.concurrency1 || "3", 10);
15966
+ const concurrency2 = parseInt(args.concurrency2 || "10", 10);
15967
+ const naiveBatches = Math.ceil(taskCount / concurrency1);
15968
+ const naiveTime = naiveBatches * 60;
15969
+ const smartBatches = Math.ceil(taskCount / concurrency2);
15970
+ const smartTime = smartBatches * 60 + 30;
15971
+ const timeDiff = naiveTime - smartTime;
15972
+ const improvement = (timeDiff / naiveTime * 100).toFixed(1);
15973
+ return `\u{1F4CA} **Strategy Comparison for ${taskCount} tasks**
15974
+
15975
+ **Strategy 1: Naive (Current)**
15976
+ | Metric | Value |
15977
+ |---------|--------|
15978
+ | Concurrency | ${concurrency1} |
15979
+ | Batches | ${naiveBatches} |
15980
+ | Est. Time | ${naiveTime}s |
15981
+
15982
+ **Strategy 2: Smart Batch (Proposed)**
15983
+ | Metric | Value |
15984
+ |---------|--------|
15985
+ | Concurrency | ${concurrency2} |
15986
+ | Batches | ${smartBatches} |
15987
+ | Est. Time | ${smartTime}s |
15988
+
15989
+ **Summary**
15990
+ | Metric | Value |
15991
+ |---------|--------|
15992
+ | Time Saved | ${timeDiff}s (${improvement}%) |
15993
+ | Batches Saved | ${naiveBatches - smartBatches} |
15994
+
15995
+ Recommendation: Use **Smart Batch** with concurrency ${concurrency2} for ${timeDiff}s improvement.`;
15996
+ }
15997
+ });
15998
+ function createBatchTools(parallelAgentManager2, client) {
15999
+ return {
16000
+ process_batch: createProcessBatchTool(parallelAgentManager2, client),
16001
+ export_failed_tasks: createExportFailedTasksTool(parallelAgentManager2),
16002
+ compare_strategies: createCompareStrategiesTool(parallelAgentManager2)
16003
+ };
16004
+ }
16005
+
16006
+ // src/tools/config.ts
16007
+ var createShowConfigTool = () => tool({
16008
+ description: `Display current OpenCode Orchestrator configuration.
16009
+
16010
+ Shows all dynamic settings including timeouts, concurrency limits, and debug flags.`,
16011
+ args: {},
16012
+ async execute() {
16013
+ configManager.exportConfigs();
16014
+ return "";
16015
+ }
16016
+ });
16017
+ var createSetConcurrencyTool = (client) => tool({
16018
+ description: `Update concurrency limit for a specific agent type.
16019
+
16020
+ <examples>
16021
+ set_concurrency({ agent: "builder", limit: 5 })
16022
+ set_concurrency({ agent: "inspector", limit: 2 })
16023
+ </examples>
16024
+
16025
+ <notes>
16026
+ - Changes take effect immediately
16027
+ - Queued tasks will start when slots become available
16028
+ - Limit cannot exceed global max (10)
16029
+ </notes>`,
16030
+ args: {
16031
+ agent: tool.schema.string().describe('Agent type (e.g., "builder", "inspector", "architect")'),
16032
+ limit: tool.schema.string().describe("New concurrency limit (number)")
16033
+ },
16034
+ async execute(args) {
16035
+ const { agent, limit } = args;
16036
+ const numLimit = parseInt(limit, 10);
16037
+ if (isNaN(numLimit) || numLimit < 1) {
16038
+ return `\u274C Invalid limit: "${limit}". Must be a number >= 1.`;
16039
+ }
16040
+ const maxLimit = configManager.getParallelAgentConfig().maxConcurrency;
16041
+ if (numLimit > maxLimit) {
16042
+ return `\u274C Limit ${numLimit} exceeds global max ${maxLimit}. Using ${maxLimit}.`;
16043
+ }
16044
+ configManager.updateParallelAgentConfig({
16045
+ defaultConcurrency: numLimit
16046
+ });
16047
+ return `\u2705 **Concurrency Updated**
16048
+
16049
+ | Property | Value |
16050
+ |----------|-------|
16051
+ | **Agent** | ${agent} |
16052
+ | **New Limit** | ${numLimit} parallel tasks |
16053
+
16054
+ Changes take effect immediately. New tasks will respect to the new limit.`;
16055
+ }
16056
+ });
16057
+ var createSetTimeoutTool = () => tool({
16058
+ description: `Update task timeout duration.
16059
+
16060
+ <examples>
16061
+ set_timeout({ taskTtlMinutes: 45 })
16062
+ set_timeout({ cleanupDelayMinutes: 2 })
16063
+ </examples>`,
16064
+ args: {
16065
+ taskTtlMinutes: tool.schema.string().optional().describe("Task timeout in minutes (default: 30)"),
16066
+ cleanupDelayMinutes: tool.schema.string().optional().describe("Cleanup delay after completion in minutes (default: 5)")
16067
+ },
16068
+ async execute(args) {
16069
+ const { taskTtlMinutes, cleanupDelayMinutes } = args;
16070
+ const updates = {};
16071
+ if (taskTtlMinutes) {
16072
+ const ttl = parseInt(taskTtlMinutes, 10);
16073
+ if (isNaN(ttl) || ttl < 1) {
16074
+ return `\u274C Invalid taskTtlMinutes: "${taskTtlMinutes}". Must be number >= 1.`;
16075
+ }
16076
+ updates.taskTtlMs = ttl * 60 * 1e3;
16077
+ }
16078
+ if (cleanupDelayMinutes) {
16079
+ const delay = parseInt(cleanupDelayMinutes, 10);
16080
+ if (isNaN(delay) || delay < 0) {
16081
+ return `\u274C Invalid cleanupDelayMinutes: "${cleanupDelayMinutes}". Must be number >= 0.`;
16082
+ }
16083
+ updates.cleanupDelayMs = delay * 60 * 1e3;
16084
+ }
16085
+ if (Object.keys(updates).length === 0) {
16086
+ return "\u26A0\uFE0F No changes specified. Provide at least one parameter.";
16087
+ }
16088
+ configManager.updateParallelAgentConfig(updates);
16089
+ let output = "\u2705 **Timeout Configuration Updated**\n\n";
16090
+ if (updates.taskTtlMs) {
16091
+ output += `| Task TTL | ${updates.taskTtlMs / 60 / 1e3} minutes |
16092
+ `;
16093
+ }
16094
+ if (updates.cleanupDelayMs) {
16095
+ output += `| Cleanup Delay | ${updates.cleanupDelayMs / 60 / 1e3} minutes |
16096
+ `;
16097
+ }
16098
+ output += "\nChanges take effect immediately.";
16099
+ return output;
16100
+ }
16101
+ });
16102
+ var createSetDebugTool = () => tool({
16103
+ description: `Enable or disable debug logging.
16104
+
16105
+ <examples>
16106
+ set_debug({ component: "parallel_agent", enable: true })
16107
+ set_debug({ component: "background_task", enable: false })
16108
+ </examples>`,
16109
+ args: {
16110
+ component: tool.schema.string().describe('Component to debug: "parallel_agent", "background_task", or "all"'),
16111
+ enable: tool.schema.boolean().describe("Enable (true) or disable (false) debug logs")
16112
+ },
16113
+ async execute(args) {
16114
+ const { component, enable } = args;
16115
+ const enableValue = enable ? "true" : "false";
16116
+ if (component === "parallel_agent" || component === "all") {
16117
+ process.env.OPENCODE_DEBUG_PARALLEL = enableValue;
16118
+ configManager.updateParallelAgentConfig({
16119
+ enableDebug: enable
16120
+ });
16121
+ }
16122
+ if (component === "background_task" || component === "all") {
16123
+ process.env.OPENCODE_DEBUG_BACKGROUND = enableValue;
16124
+ configManager.updateBackgroundTaskConfig({
16125
+ enableDebug: enable
16126
+ });
16127
+ }
16128
+ return `\u2705 **Debug Logging ${enable ? "Enabled" : "Disabled"}**
16129
+
16130
+ | Component | Debug Status |
16131
+ |-----------|--------------|
16132
+ | ${component === "all" ? "parallel_agent" : component} | ${enable ? "\u{1F527} Enabled" : "\u{1F507} Disabled"} |
16133
+ ${component === "all" ? "| background_task | \u{1F527} Enabled |" : ""}
16134
+
16135
+ Changes take effect immediately.`;
16136
+ }
16137
+ });
16138
+ function createConfigTools(client) {
16139
+ return {
16140
+ show_config: createShowConfigTool(),
16141
+ set_concurrency: createSetConcurrencyTool(client),
16142
+ set_timeout: createSetTimeoutTool(),
16143
+ set_debug: createSetDebugTool()
16144
+ };
16145
+ }
16146
+
15477
16147
  // src/utils/common.ts
15478
16148
  function detectSlashCommand(text) {
15479
16149
  const match = text.trim().match(/^\/([a-zA-Z0-9_-]+)(?:\s+(.*))?$/);
@@ -15508,12 +16178,30 @@ var AGENT_EMOJI2 = {
15508
16178
  [AGENT_NAMES.RECORDER]: "\u{1F4BE}",
15509
16179
  [AGENT_NAMES.COMMANDER]: "\u{1F3AF}"
15510
16180
  };
16181
+ var CONTINUE_INSTRUCTION = `<auto_continue>
16182
+ <status>Mission not complete. Keep executing.</status>
16183
+
16184
+ <rules>
16185
+ 1. DO NOT stop - mission is incomplete
16186
+ 2. DO NOT wait for user input
16187
+ 3. If previous action failed, try different approach
16188
+ 4. If agent returned nothing, proceed to next step
16189
+ </rules>
16190
+
16191
+ <next_step>
16192
+ What is the current state?
16193
+ What is the next action?
16194
+ Execute it NOW.
16195
+ </next_step>
16196
+ </auto_continue>`;
15511
16197
  var OrchestratorPlugin = async (input) => {
15512
16198
  const { directory, client } = input;
15513
16199
  console.log(`[orchestrator] v${PLUGIN_VERSION} loaded`);
15514
16200
  const sessions = /* @__PURE__ */ new Map();
15515
16201
  const parallelAgentManager2 = ParallelAgentManager.getInstance(client, directory);
15516
16202
  const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client);
16203
+ const batchTools = createBatchTools(parallelAgentManager2, client);
16204
+ const configTools = createConfigTools(client);
15517
16205
  return {
15518
16206
  // -----------------------------------------------------------------
15519
16207
  // Tools we expose to the LLM
@@ -15531,13 +16219,13 @@ var OrchestratorPlugin = async (input) => {
15531
16219
  list_background: listBackgroundTool,
15532
16220
  kill_background: killBackgroundTool,
15533
16221
  // Async agent tools - spawn agents in parallel sessions
15534
- ...asyncAgentTools
16222
+ ...asyncAgentTools,
15535
16223
  // Git tools - branch info and status
15536
- // git_branch: gitBranchTool(directory),
16224
+ git_branch: gitBranchTool(directory),
15537
16225
  // Smart batch tools - centralized validation and retry
15538
- // ...batchTools,
16226
+ ...batchTools,
15539
16227
  // Configuration tools - dynamic runtime settings
15540
- // ...configTools,
16228
+ ...configTools
15541
16229
  },
15542
16230
  // -----------------------------------------------------------------
15543
16231
  // Config hook - registers our commands and agents with OpenCode
@@ -15748,17 +16436,107 @@ ${stateSession.graph.getTaskSummary()}`;
15748
16436
  \u23F1\uFE0F [${currentTime}] Step ${session.step}/${session.maxSteps} | This step: ${stepDuration} | Total: ${totalElapsed}`;
15749
16437
  },
15750
16438
  // -----------------------------------------------------------------
15751
- // NOTE: assistant.done hook has been REMOVED
15752
- // It was NOT in the official OpenCode plugin API and was causing
15753
- // UI rendering issues. The "relentless loop" feature needs to be
15754
- // reimplemented using supported hooks like tool.execute.after
15755
- // or the event hook.
16439
+ // assistant.done hook - runs when the LLM finishes responding
16440
+ // This is the heart of the "relentless loop" - we keep pushing it
16441
+ // to continue until we see MISSION COMPLETE or hit the limit
15756
16442
  // -----------------------------------------------------------------
16443
+ "assistant.done": async (assistantInput, assistantOutput) => {
16444
+ const sessionID = assistantInput.sessionID;
16445
+ const session = sessions.get(sessionID);
16446
+ if (!session?.active) return;
16447
+ const parts = assistantOutput.parts;
16448
+ const textContent = parts?.filter((p) => p.type === "text" || p.type === "reasoning").map((p) => p.text || "").join("\n") || "";
16449
+ const stateSession = state.sessions.get(sessionID);
16450
+ const sanityResult = checkOutputSanity(textContent);
16451
+ if (!sanityResult.isHealthy && stateSession) {
16452
+ stateSession.anomalyCount = (stateSession.anomalyCount || 0) + 1;
16453
+ session.step++;
16454
+ session.timestamp = Date.now();
16455
+ const recoveryText = stateSession.anomalyCount >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT;
16456
+ try {
16457
+ if (client?.session?.prompt) {
16458
+ await client.session.prompt({
16459
+ path: { id: sessionID },
16460
+ body: {
16461
+ parts: [{
16462
+ type: "text",
16463
+ text: `\u26A0\uFE0F ANOMALY #${stateSession.anomalyCount}: ${sanityResult.reason}
16464
+
16465
+ ` + recoveryText + `
16466
+
16467
+ [Recovery Step ${session.step}/${session.maxSteps}]`
16468
+ }]
16469
+ }
16470
+ });
16471
+ }
16472
+ } catch {
16473
+ session.active = false;
16474
+ state.missionActive = false;
16475
+ }
16476
+ return;
16477
+ }
16478
+ if (stateSession && stateSession.anomalyCount > 0) {
16479
+ stateSession.anomalyCount = 0;
16480
+ }
16481
+ if (textContent.includes("\u2705 MISSION COMPLETE") || textContent.includes("MISSION COMPLETE")) {
16482
+ session.active = false;
16483
+ state.missionActive = false;
16484
+ sessions.delete(sessionID);
16485
+ state.sessions.delete(sessionID);
16486
+ return;
16487
+ }
16488
+ if (textContent.includes("/stop") || textContent.includes("/cancel")) {
16489
+ session.active = false;
16490
+ state.missionActive = false;
16491
+ sessions.delete(sessionID);
16492
+ state.sessions.delete(sessionID);
16493
+ return;
16494
+ }
16495
+ const now = Date.now();
16496
+ const stepDuration = formatElapsedTime(session.lastStepTime, now);
16497
+ const totalElapsed = formatElapsedTime(session.startTime, now);
16498
+ session.step++;
16499
+ session.timestamp = now;
16500
+ session.lastStepTime = now;
16501
+ const currentTime = formatTimestamp();
16502
+ if (session.step >= session.maxSteps) {
16503
+ session.active = false;
16504
+ state.missionActive = false;
16505
+ return;
16506
+ }
16507
+ try {
16508
+ if (client?.session?.prompt) {
16509
+ await client.session.prompt({
16510
+ path: { id: sessionID },
16511
+ body: {
16512
+ parts: [{
16513
+ type: "text",
16514
+ text: CONTINUE_INSTRUCTION + `
16515
+
16516
+ \u23F1\uFE0F [${currentTime}] Step ${session.step}/${session.maxSteps} | This step: ${stepDuration} | Total: ${totalElapsed}`
16517
+ }]
16518
+ }
16519
+ });
16520
+ }
16521
+ } catch {
16522
+ try {
16523
+ await new Promise((r) => setTimeout(r, 500));
16524
+ if (client?.session?.prompt) {
16525
+ await client.session.prompt({
16526
+ path: { id: sessionID },
16527
+ body: { parts: [{ type: "text", text: "continue" }] }
16528
+ });
16529
+ }
16530
+ } catch {
16531
+ session.active = false;
16532
+ state.missionActive = false;
16533
+ }
16534
+ }
16535
+ },
15757
16536
  // -----------------------------------------------------------------
15758
16537
  // Event handler - cleans up when sessions are deleted
15759
16538
  // -----------------------------------------------------------------
15760
- event: async (input2) => {
15761
- const { event } = input2;
16539
+ handler: async ({ event }) => {
15762
16540
  if (event.type === "session.deleted") {
15763
16541
  const props = event.properties;
15764
16542
  if (props?.info?.id) {
@@ -37,13 +37,13 @@ export declare const listBackgroundTool: {
37
37
  args: {
38
38
  status: import("zod").ZodOptional<import("zod").ZodEnum<{
39
39
  running: "running";
40
+ all: "all";
40
41
  done: "done";
41
42
  error: "error";
42
- all: "all";
43
43
  }>>;
44
44
  };
45
45
  execute(args: {
46
- status?: "running" | "done" | "error" | "all" | undefined;
46
+ status?: "running" | "all" | "done" | "error" | undefined;
47
47
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
48
48
  };
49
49
  export declare const killBackgroundTool: {
@@ -33,16 +33,16 @@ export declare const gitBranchTool: (directory: string) => {
33
33
  args: {
34
34
  action: import("zod").ZodEnum<{
35
35
  status: "status";
36
- all: "all";
37
36
  diff: "diff";
38
37
  current: "current";
39
38
  list: "list";
40
39
  recent: "recent";
40
+ all: "all";
41
41
  }>;
42
42
  baseBranch: import("zod").ZodOptional<import("zod").ZodString>;
43
43
  };
44
44
  execute(args: {
45
- action: "status" | "all" | "diff" | "current" | "list" | "recent";
45
+ action: "status" | "diff" | "current" | "list" | "recent" | "all";
46
46
  baseBranch?: string | undefined;
47
47
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
48
48
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "opencode-orchestrator",
3
3
  "displayName": "OpenCode Orchestrator",
4
4
  "description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
5
- "version": "0.5.2",
5
+ "version": "0.5.3",
6
6
  "author": "agnusdei1207",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -51,8 +51,7 @@
51
51
  "dev:status": "echo '=== Global Link Status ===' && shx ls -l $(npm root -g)/opencode-orchestrator || echo 'Not linked'",
52
52
  "dev:test": "node dist/scripts/postinstall.js && echo '---' && node dist/scripts/preuninstall.js",
53
53
  "delete": "brew uninstall opencode && rm -rf ~/.config/opencode && npm uninstall -g opencode-orchestrator",
54
- "setup": "brew install opencode && npm install -g opencode-orchestrator",
55
- "set": "npm run delete && npm run setup"
54
+ "install": "brew install opencode && npm install -g opencode-orchestrator"
56
55
  },
57
56
  "dependencies": {
58
57
  "@opencode-ai/plugin": "^1.1.1",