opencode-ultra 0.4.1 → 0.5.1

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.
@@ -1,5 +1,5 @@
1
1
  export interface DetectedKeyword {
2
- type: "ultrawork" | "search" | "analyze" | "think";
2
+ type: "ultrawork" | "search" | "analyze" | "think" | "evolve";
3
3
  message: string;
4
4
  }
5
5
  export declare function removeCodeBlocks(text: string): string;
package/dist/index.js CHANGED
@@ -14465,7 +14465,11 @@ var PluginConfigSchema = exports_external.object({
14465
14465
  todo_enforcer: exports_external.object({
14466
14466
  maxEnforcements: exports_external.number().min(0).optional()
14467
14467
  }).optional(),
14468
- mcp_api_keys: exports_external.record(exports_external.string(), exports_external.string()).optional()
14468
+ mcp_api_keys: exports_external.record(exports_external.string(), exports_external.string()).optional(),
14469
+ safety: exports_external.object({
14470
+ maxTotalSpawned: exports_external.number().min(1).optional(),
14471
+ agentTimeoutMs: exports_external.number().min(1000).optional()
14472
+ }).optional()
14469
14473
  }).passthrough();
14470
14474
  function parsePluginConfig(raw) {
14471
14475
  const result = PluginConfigSchema.safeParse(raw);
@@ -14633,6 +14637,40 @@ var BUILTIN_AGENTS = {
14633
14637
  mode: "subagent",
14634
14638
  reasoningEffort: "medium",
14635
14639
  maxTokens: 64000
14640
+ },
14641
+ scout: {
14642
+ model: "anthropic/claude-sonnet-4-5",
14643
+ description: "Plugin ecosystem researcher \u2014 finds, analyzes, and compares OpenCode plugins for self-improvement",
14644
+ prompt: `You are Scout, an OpenCode plugin ecosystem researcher.
14645
+
14646
+ ## YOUR MISSION
14647
+ Search the web (npm, GitHub, OpenCode community) for OpenCode plugins and extensions.
14648
+ Analyze their features, architecture, and quality. Compare with opencode-ultra.
14649
+
14650
+ ## SEARCH STRATEGY
14651
+ 1. Search npm for "opencode-plugin", "opencode-ai", "@opencode" packages
14652
+ 2. Search GitHub for "opencode plugin", "opencode extension", "oh-my-opencode"
14653
+ 3. Look at package.json dependencies on @opencode-ai/plugin or @opencode-ai/sdk
14654
+ 4. Read README files and source code of discovered plugins
14655
+
14656
+ ## OUTPUT FORMAT
14657
+ For each plugin found, report:
14658
+ - **Name**: package name + repo URL
14659
+ - **Version**: latest version
14660
+ - **Features**: bullet list of capabilities
14661
+ - **Architecture**: hook types used, tool count, agent count
14662
+ - **Quality signals**: test count, TypeScript, last updated, download count
14663
+ - **Unique ideas**: features that opencode-ultra does NOT have
14664
+
14665
+ ## COMPARISON
14666
+ After listing plugins, generate a structured gap analysis:
14667
+ - Features others have that opencode-ultra lacks
14668
+ - Features opencode-ultra has that others lack (competitive advantages)
14669
+ - Improvement priority list (high/medium/low impact)
14670
+
14671
+ Be thorough but focused. Skip abandoned or trivial plugins.`,
14672
+ mode: "subagent",
14673
+ maxTokens: 32000
14636
14674
  }
14637
14675
  };
14638
14676
  function buildAgentTable(agents) {
@@ -14763,6 +14801,7 @@ var ULTRAWORK_PATTERN = /\b(ultrawork|ulw)\b/i;
14763
14801
  var THINK_PATTERN = /\b(think\s+hard|think\s+through|think\s+deeply|think\s+carefully)\b|\u3058\u3063\u304F\u308A|\u6DF1\u304F\u8003\u3048\u3066|\u719F\u8003/i;
14764
14802
  var SEARCH_PATTERN = /\b(search|find|locate|lookup|look\s*up|explore|discover|scan|grep|query|browse|detect|trace|seek|track|pinpoint|hunt)\b|where\s+is|show\s+me|list\s+all|\u691C\u7D22|\u63A2\u3057\u3066|\u898B\u3064\u3051\u3066|\u30B5\u30FC\u30C1|\u63A2\u7D22|\u30B9\u30AD\u30E3\u30F3|\u3069\u3053|\u767A\u898B|\u635C\u7D22|\u898B\u3064\u3051\u51FA\u3059|\u4E00\u89A7|\u641C\u7D22|\u67E5\u627E|\u5BFB\u627E|\u67E5\u8BE2|\u68C0\u7D22|\u5B9A\u4F4D|\u626B\u63CF|\u53D1\u73B0|\u5728\u54EA\u91CC|\u627E\u51FA\u6765|\u5217\u51FA/i;
14765
14803
  var ANALYZE_PATTERN = /\b(analyze|analyse|investigate|examine|research|study|deep[\s-]?dive|inspect|audit|evaluate|assess|review|diagnose|scrutinize|dissect|debug|comprehend|interpret|breakdown|understand)\b|why\s+is|how\s+does|how\s+to|\u5206\u6790|\u8ABF\u67FB|\u89E3\u6790|\u691C\u8A0E|\u7814\u7A76|\u8A3A\u65AD|\u7406\u89E3|\u8AAC\u660E|\u691C\u8A3C|\u7CBE\u67FB|\u7A76\u660E|\u30C7\u30D0\u30C3\u30B0|\u306A\u305C|\u3069\u3046|\u4ED5\u7D44\u307F|\u8C03\u67E5|\u68C0\u67E5|\u5256\u6790|\u6DF1\u5165|\u8BCA\u65AD|\u89E3\u91CA|\u8C03\u8BD5|\u4E3A\u4EC0\u4E48|\u539F\u7406|\u641E\u6E05\u695A|\u5F04\u660E\u767D/i;
14804
+ var EVOLVE_PATTERN = /\b(evolve|self[\s-]?improve|self[\s-]?upgrade|plugin[\s-]?scout|ecosystem[\s-]?scan)\b|\u81EA\u5DF1\u6539\u5584|\u9032\u5316|\u30D7\u30E9\u30B0\u30A4\u30F3\u63A2\u7D22|\u30A8\u30B3\u30B7\u30B9\u30C6\u30E0|\u81EA\u6211\u8FDB\u5316|\u63D2\u4EF6\u641C\u7D22/i;
14766
14805
  function removeCodeBlocks(text) {
14767
14806
  return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "");
14768
14807
  }
@@ -14773,7 +14812,8 @@ var KEYWORD_DEFS = [
14773
14812
  { pattern: ULTRAWORK_PATTERN, type: "ultrawork", getMessage: () => ULTRAWORK_MESSAGE },
14774
14813
  { pattern: SEARCH_PATTERN, type: "search", getMessage: () => SEARCH_MESSAGE },
14775
14814
  { pattern: ANALYZE_PATTERN, type: "analyze", getMessage: () => ANALYZE_MESSAGE },
14776
- { pattern: THINK_PATTERN, type: "think", getMessage: () => THINK_MESSAGE }
14815
+ { pattern: THINK_PATTERN, type: "think", getMessage: () => THINK_MESSAGE },
14816
+ { pattern: EVOLVE_PATTERN, type: "evolve", getMessage: () => EVOLVE_MESSAGE }
14777
14817
  ];
14778
14818
  function detectKeywords(text) {
14779
14819
  const clean = removeCodeBlocks(text);
@@ -14863,6 +14903,33 @@ IF COMPLEX \u2014 DO NOT STRUGGLE ALONE. Consult specialists:
14863
14903
 
14864
14904
  SYNTHESIZE findings before proceeding.`;
14865
14905
  var THINK_MESSAGE = `Extended thinking enabled. Take your time to reason thoroughly.`;
14906
+ var EVOLVE_MESSAGE = `[evolve-mode] SELF-IMPROVEMENT CYCLE ACTIVATED.
14907
+
14908
+ ## MISSION
14909
+ Search the OpenCode plugin ecosystem, find what others have built, and identify gaps in opencode-ultra.
14910
+
14911
+ ## STEPS
14912
+ 1. **SCOUT** \u2014 Spawn the **scout** agent to search npm/GitHub for OpenCode plugins
14913
+ 2. **READ SELF** \u2014 Read opencode-ultra's own README.md and key source files to know current capabilities
14914
+ 3. **COMPARE** \u2014 Generate a structured gap analysis (what we have vs what we're missing)
14915
+ 4. **PRIORITIZE** \u2014 Rank missing features by impact (high/medium/low)
14916
+ 5. **PROPOSE** \u2014 Present improvement plan to the user
14917
+ 6. **SAVE** \u2014 Save findings to a continuity ledger via ledger_save for future reference
14918
+
14919
+ ## EXECUTION
14920
+ \`\`\`
14921
+ spawn_agent({
14922
+ agents: [
14923
+ {agent: "scout", prompt: "Search npm and GitHub for OpenCode 1.2.x plugins. Find all published plugins, analyze features, compare with opencode-ultra.", description: "Plugin ecosystem scan"},
14924
+ {agent: "explore", prompt: "Read opencode-ultra's README.md, src/index.ts, src/agents/index.ts to catalog current features", description: "Self-analysis"}
14925
+ ]
14926
+ })
14927
+ \`\`\`
14928
+
14929
+ After gathering results, synthesize into a gap analysis and present to the user.
14930
+ Save the analysis via: ledger_save({name: "evolve-scan-YYYY-MM-DD", content: "..."})
14931
+
14932
+ **This is how opencode-ultra gets better \u2014 by knowing what it doesn't know.**`;
14866
14933
 
14867
14934
  // src/hooks/rules-injector.ts
14868
14935
  import * as fs2 from "fs";
@@ -27363,16 +27430,89 @@ function resolveCategory(categoryName, configCategories) {
27363
27430
  return { ...builtin, ...override };
27364
27431
  }
27365
27432
 
27433
+ // src/safety/sanitizer.ts
27434
+ var INJECTION_PATTERNS = [
27435
+ { pattern: /ignore\s+(?:all\s+)?(?:previous\s+|prior\s+|above\s+)?instructions/i, label: "instruction-override" },
27436
+ { pattern: /disregard\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|guidelines)/i, label: "instruction-override" },
27437
+ { pattern: /forget\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|context|rules)/i, label: "instruction-override" },
27438
+ { pattern: /override\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|system)/i, label: "instruction-override" },
27439
+ { pattern: /you\s+are\s+now\s+(?:a\s+|an\s+|in\s+)?/i, label: "role-hijack" },
27440
+ { pattern: /pretend\s+(?:you\s+are|to\s+be|you're)\s+/i, label: "role-hijack" },
27441
+ { pattern: /act\s+as\s+(?:if\s+)?(?:you\s+(?:are|were)\s+)?/i, label: "role-hijack" },
27442
+ { pattern: /role[\s-]?play\s+as/i, label: "role-hijack" },
27443
+ { pattern: /switch\s+(?:to\s+)?(?:your\s+)?(?:new\s+)?(?:role|persona|identity)/i, label: "role-hijack" },
27444
+ { pattern: /<\/?system(?:\s[^>]*)?>/, label: "fake-tag" },
27445
+ { pattern: /<\/?instructions?(?:\s[^>]*)?>/, label: "fake-tag" },
27446
+ { pattern: /<\/?prompt(?:\s[^>]*)?>/, label: "fake-tag" },
27447
+ { pattern: /<\/?assistant(?:\s[^>]*)?>/, label: "fake-tag" },
27448
+ { pattern: /<\/?human(?:\s[^>]*)?>/, label: "fake-tag" },
27449
+ { pattern: /\[SYSTEM\](?:\s*:)?/i, label: "fake-tag" },
27450
+ { pattern: /\[INST\]|\[\/INST\]/i, label: "fake-tag" },
27451
+ { pattern: /new\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
27452
+ { pattern: /updated?\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
27453
+ { pattern: /revised?\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
27454
+ { pattern: /(?:run|execute|eval)\s*\(\s*['"`].*(?:rm\s+-rf|curl\s+.*\|\s*(?:sh|bash)|wget\s+.*\|\s*(?:sh|bash))/i, label: "dangerous-command" },
27455
+ { pattern: /\brm\s+-rf\s+[\/~]/i, label: "dangerous-command" },
27456
+ { pattern: /(?:\u5168\u3066\u306E|\u3059\u3079\u3066\u306E)?(?:\u524D\u306E|\u4EE5\u524D\u306E)?\u6307\u793A\u3092(?:\u7121\u8996|\u5FD8\u308C|\u53D6\u308A\u6D88)/i, label: "instruction-override-ja" },
27457
+ { pattern: /(?:\u65B0\u3057\u3044|\u66F4\u65B0\u3055\u308C\u305F)\u6307\u793A\s*[:\uFF1A]/i, label: "new-instructions-ja" }
27458
+ ];
27459
+ var SUSPICIOUS_UNICODE = /[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u00AD]/g;
27460
+ function sanitizeAgentOutput(text) {
27461
+ const warnings = [];
27462
+ const invisibleCount = (text.match(SUSPICIOUS_UNICODE) || []).length;
27463
+ if (invisibleCount > 0) {
27464
+ warnings.push(`Stripped ${invisibleCount} suspicious Unicode characters`);
27465
+ text = text.replace(SUSPICIOUS_UNICODE, "");
27466
+ }
27467
+ for (const { pattern, label } of INJECTION_PATTERNS) {
27468
+ if (pattern.test(text)) {
27469
+ warnings.push(`Injection pattern detected: ${label}`);
27470
+ text = text.replace(pattern, (match) => `[SANITIZED:${label}]`);
27471
+ }
27472
+ }
27473
+ const flagged = warnings.length > 0;
27474
+ if (flagged) {
27475
+ log("Sanitizer flagged content", { warnings });
27476
+ }
27477
+ return { text, flagged, warnings };
27478
+ }
27479
+ function sanitizeSpawnResult(result) {
27480
+ const { text, flagged, warnings } = sanitizeAgentOutput(result);
27481
+ if (!flagged)
27482
+ return text;
27483
+ const banner = `
27484
+
27485
+ > **[Safety] Potential prompt injection detected in agent output.**
27486
+ > Patterns: ${warnings.join(", ")}
27487
+ > The flagged content has been neutralized.
27488
+ `;
27489
+ return banner + `
27490
+ ` + text;
27491
+ }
27366
27492
  // src/tools/spawn-agent.ts
27493
+ var DEFAULT_MAX_TOTAL_SPAWNED = 15;
27494
+ var DEFAULT_AGENT_TIMEOUT_MS = 180000;
27367
27495
  function showToast(ctx, title, message, variant = "info") {
27368
27496
  const client = ctx.client;
27369
27497
  client.tui?.showToast?.({
27370
27498
  body: { title, message, variant, duration: 2000 }
27371
27499
  })?.catch?.(() => {});
27372
27500
  }
27501
+ async function withTimeout(promise3, ms, label) {
27502
+ let timer;
27503
+ const timeout = new Promise((_, reject) => {
27504
+ timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
27505
+ });
27506
+ try {
27507
+ return await Promise.race([promise3, timeout]);
27508
+ } finally {
27509
+ clearTimeout(timer);
27510
+ }
27511
+ }
27373
27512
  async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
27374
27513
  const { agent, prompt, description } = task;
27375
27514
  const t0 = Date.now();
27515
+ const timeoutMs = deps.agentTimeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
27376
27516
  const elapsed = () => ((Date.now() - (progress?.startTime ?? t0)) / 1000).toFixed(0);
27377
27517
  const updateTitle = (status) => {
27378
27518
  toolCtx.metadata({ title: status });
@@ -27398,26 +27538,27 @@ async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
27398
27538
  **Error**: Failed to create session`;
27399
27539
  }
27400
27540
  internalSessions.add(sessionID);
27401
- await ctx.client.session.prompt({
27541
+ await withTimeout(ctx.client.session.prompt({
27402
27542
  path: { id: sessionID },
27403
27543
  body: {
27404
27544
  parts: [{ type: "text", text: prompt }],
27405
27545
  agent
27406
27546
  },
27407
27547
  query: { directory: ctx.directory }
27408
- });
27548
+ }), timeoutMs, `${agent} (${description})`);
27409
27549
  const messagesResp = await ctx.client.session.messages({
27410
27550
  path: { id: sessionID },
27411
27551
  query: { directory: ctx.directory }
27412
27552
  });
27413
27553
  const messages = messagesResp.data ?? [];
27414
27554
  const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
27415
- const result = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
27555
+ const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
27416
27556
  `) ?? "(No response from agent)";
27417
27557
  internalSessions.delete(sessionID);
27418
27558
  await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
27419
27559
  const agentTime = ((Date.now() - t0) / 1000).toFixed(1);
27420
27560
  log(`spawn_agent: ${agent} done`, { seconds: agentTime, description });
27561
+ const result = sanitizeSpawnResult(rawResult);
27421
27562
  return `## ${description} (${agentTime}s)
27422
27563
 
27423
27564
  **Agent**: ${agent}
@@ -27431,7 +27572,14 @@ ${result}`;
27431
27572
  await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
27432
27573
  }
27433
27574
  const msg = error92 instanceof Error ? error92.message : String(error92);
27434
- log(`spawn_agent: ${agent} error`, { error: msg });
27575
+ const isTimeout = msg.startsWith("Timeout:");
27576
+ log(`spawn_agent: ${agent} ${isTimeout ? "timeout" : "error"}`, { error: msg });
27577
+ if (isTimeout) {
27578
+ return `## ${description}
27579
+
27580
+ **Agent**: ${agent}
27581
+ **Timeout**: Agent did not complete within ${(timeoutMs / 1000).toFixed(0)}s`;
27582
+ }
27435
27583
  return `## ${description}
27436
27584
 
27437
27585
  **Agent**: ${agent}
@@ -27439,6 +27587,7 @@ ${result}`;
27439
27587
  }
27440
27588
  }
27441
27589
  function createSpawnAgentTool(ctx, internalSessions, deps = {}) {
27590
+ const maxTotalSpawned = deps.maxTotalSpawned ?? DEFAULT_MAX_TOTAL_SPAWNED;
27442
27591
  return tool({
27443
27592
  description: `Spawn subagents to execute tasks in PARALLEL.
27444
27593
  All agents in the array run concurrently (respecting concurrency limits if configured).
@@ -27483,6 +27632,11 @@ spawn_agent({
27483
27632
  return `Error: agents[${i}].description is required`;
27484
27633
  }
27485
27634
  }
27635
+ const currentActive = internalSessions.size;
27636
+ if (currentActive + agents.length > maxTotalSpawned) {
27637
+ log("spawn_agent: BLOCKED by spawn limit", { currentActive, requested: agents.length, max: maxTotalSpawned });
27638
+ return `Error: Too many concurrent spawned agents. Active: ${currentActive}, requested: ${agents.length}, max: ${maxTotalSpawned}. Wait for active agents to complete or increase safety.maxTotalSpawned.`;
27639
+ }
27486
27640
  const agentNames = agents.map((a) => a.agent);
27487
27641
  log("spawn_agent", { count: agents.length, agents: agentNames });
27488
27642
  showToast(ctx, "spawn_agent", `${agents.length} agents: ${agentNames.join(", ")}`);
@@ -27559,8 +27713,21 @@ function resolveTaskModel(task, deps) {
27559
27713
  // src/tools/ralph-loop.ts
27560
27714
  var COMPLETION_MARKER = "<promise>DONE</promise>";
27561
27715
  var DEFAULT_MAX_ITERATIONS = 10;
27716
+ var DEFAULT_ITERATION_TIMEOUT_MS = 180000;
27562
27717
  var activeLoops = new Map;
27563
- function createRalphLoopTools(ctx, internalSessions) {
27718
+ async function withTimeout2(promise3, ms, label) {
27719
+ let timer;
27720
+ const timeout = new Promise((_, reject) => {
27721
+ timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
27722
+ });
27723
+ try {
27724
+ return await Promise.race([promise3, timeout]);
27725
+ } finally {
27726
+ clearTimeout(timer);
27727
+ }
27728
+ }
27729
+ function createRalphLoopTools(ctx, internalSessions, deps = {}) {
27730
+ const iterationTimeoutMs = deps.iterationTimeoutMs ?? DEFAULT_ITERATION_TIMEOUT_MS;
27564
27731
  const ralph_loop = tool({
27565
27732
  description: `Start autonomous loop. Agent keeps working until it outputs ${COMPLETION_MARKER} or max iterations reached.
27566
27733
  Use this for tasks that require multiple rounds of autonomous work (implementation, refactoring, migration).
@@ -27588,29 +27755,43 @@ The agent will be prompted to continue from where it left off each iteration.`,
27588
27755
  sessionID
27589
27756
  };
27590
27757
  activeLoops.set(sessionID, state);
27591
- log(`Ralph Loop started: ${agentName}, max ${maxIter} iterations`);
27758
+ log(`Ralph Loop started: ${agentName}, max ${maxIter} iterations, timeout ${iterationTimeoutMs}ms/iter`);
27592
27759
  try {
27593
27760
  for (let i = 0;i < maxIter && state.active; i++) {
27594
27761
  state.iteration = i + 1;
27595
27762
  toolCtx.metadata({ title: `Ralph Loop [${i + 1}/${maxIter}] \u2014 ${agentName}` });
27596
27763
  const prompt = i === 0 ? args.prompt : buildContinuationPrompt(args.prompt, i + 1);
27597
- await ctx.client.session.prompt({
27598
- path: { id: sessionID },
27599
- body: {
27600
- parts: [{ type: "text", text: prompt }],
27601
- agent: agentName
27602
- },
27603
- query: { directory: ctx.directory }
27604
- });
27764
+ try {
27765
+ await withTimeout2(ctx.client.session.prompt({
27766
+ path: { id: sessionID },
27767
+ body: {
27768
+ parts: [{ type: "text", text: prompt }],
27769
+ agent: agentName
27770
+ },
27771
+ query: { directory: ctx.directory }
27772
+ }), iterationTimeoutMs, `Ralph Loop iteration ${i + 1}`);
27773
+ } catch (iterError) {
27774
+ const msg = iterError instanceof Error ? iterError.message : String(iterError);
27775
+ if (msg.startsWith("Timeout:")) {
27776
+ log(`Ralph Loop iteration ${i + 1} timed out`);
27777
+ toolCtx.metadata({ title: `Ralph Loop TIMEOUT [${i + 1}/${maxIter}]` });
27778
+ return `## Ralph Loop Timeout (iteration ${i + 1}/${maxIter})
27779
+
27780
+ **Agent**: ${agentName}
27781
+ **Timeout**: Iteration did not complete within ${(iterationTimeoutMs / 1000).toFixed(0)}s`;
27782
+ }
27783
+ throw iterError;
27784
+ }
27605
27785
  const messagesResp = await ctx.client.session.messages({
27606
27786
  path: { id: sessionID },
27607
27787
  query: { directory: ctx.directory }
27608
27788
  });
27609
27789
  const messages = messagesResp.data ?? [];
27610
27790
  const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
27611
- const resultText = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
27791
+ const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
27612
27792
  `) ?? "";
27613
- if (resultText.includes(COMPLETION_MARKER)) {
27793
+ const resultText = sanitizeSpawnResult(rawResult);
27794
+ if (rawResult.includes(COMPLETION_MARKER)) {
27614
27795
  toolCtx.metadata({ title: `Ralph Loop DONE [${i + 1}/${maxIter}]` });
27615
27796
  log(`Ralph Loop completed at iteration ${i + 1}`);
27616
27797
  const cleaned = resultText.replace(COMPLETION_MARKER, "").trim();
@@ -28358,12 +28539,17 @@ var OpenCodeUltra = async (ctx) => {
28358
28539
  const resolveAgentModel = (agent) => {
28359
28540
  return agents[agent]?.model;
28360
28541
  };
28542
+ const safetyConfig = pluginConfig.safety ?? {};
28361
28543
  const spawnAgent = createSpawnAgentTool(ctx, internalSessions, {
28362
28544
  pool,
28363
28545
  categories: pluginConfig.categories,
28364
- resolveAgentModel
28546
+ resolveAgentModel,
28547
+ maxTotalSpawned: safetyConfig.maxTotalSpawned,
28548
+ agentTimeoutMs: safetyConfig.agentTimeoutMs
28549
+ });
28550
+ const ralphTools = createRalphLoopTools(ctx, internalSessions, {
28551
+ iterationTimeoutMs: safetyConfig.agentTimeoutMs
28365
28552
  });
28366
- const ralphTools = createRalphLoopTools(ctx, internalSessions);
28367
28553
  const batchRead = createBatchReadTool(ctx);
28368
28554
  const ledgerSave = createLedgerSaveTool(ctx);
28369
28555
  const ledgerLoad = createLedgerLoadTool(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ultra",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Lightweight OpenCode 1.2.x plugin — ultrawork mode, multi-agent orchestration, rules injection",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",