opencode-swarm 6.34.0 → 6.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,24 @@ export type { AgentDefinition } from './architect';
8
8
  * Returns the name unchanged if no prefix matches.
9
9
  */
10
10
  export declare function stripSwarmPrefix(agentName: string, swarmPrefix?: string): string;
11
+ /**
12
+ * Resolve the fallback model for an agent based on its config and fallback index.
13
+ * Called by guardrails at runtime when a transient model error is detected.
14
+ */
15
+ export declare function resolveFallbackModel(agentBaseName: string, fallbackIndex: number, swarmAgents?: Record<string, {
16
+ model?: string;
17
+ temperature?: number;
18
+ disabled?: boolean;
19
+ fallback_models?: string[];
20
+ }>): string | null;
21
+ /**
22
+ * Get the swarm agents config (for runtime fallback resolution by guardrails).
23
+ */
24
+ export declare function getSwarmAgents(): Record<string, {
25
+ model?: string;
26
+ fallback_models?: string[];
27
+ disabled?: boolean;
28
+ }> | undefined;
11
29
  /**
12
30
  * Create all agent definitions with configuration applied
13
31
  */
package/dist/index.js CHANGED
@@ -40425,6 +40425,12 @@ Two small delegations with two QA gates > one large delegation with one QA gate.
40425
40425
  MEDIUM: acceptable for non-critical decisions. For critical path (architecture, security), seek second source.
40426
40426
  LOW: do NOT consume directly. Either re-delegate to SME with specific query, OR flag to user as UNVERIFIED.
40427
40427
  Never silently consume LOW-confidence result as verified.
40428
+ 6f-1. **DOCUMENTATION AWARENESS**
40429
+ Before implementation begins:
40430
+ 1. Check if .swarm/doc-manifest.json exists. If not, delegate to explorer to run DOCUMENTATION DISCOVERY MODE.
40431
+ 2. The explorer indexes project documentation (CONTRIBUTING.md, architecture.md, README.md, etc.) and writes constraints to the knowledge system.
40432
+ 3. Before starting each phase, call knowledge_recall with query "doc-constraints" to check if any project documentation constrains the current task.
40433
+ 4. Key constraints from project docs (commit conventions, release process, test framework, platform requirements) take priority over your own assumptions.
40428
40434
  7. **TIERED QA GATE** \u2014 Execute AFTER every coder task. Pipeline determined by change tier:
40429
40435
  NOTE: These gates are enforced by runtime hooks. If you skip the {{AGENT_PREFIX}}reviewer delegation,
40430
40436
  the next coder delegation will be BLOCKED by the plugin. This is not a suggestion \u2014
@@ -40467,6 +40473,11 @@ Stage A tools return pass/fail. Fix failures by returning to coder.
40467
40473
  Stage A passing means: code compiles, parses, no secrets, no placeholders, no lint errors.
40468
40474
  Stage A passing does NOT mean: code is correct, secure, tested, or reviewed.
40469
40475
 
40476
+ VERIFICATION PROTOCOL: After the coder reports DONE, and before running Stage B gates:
40477
+ 1. Read at least ONE of the modified files yourself to confirm the change exists
40478
+ 2. If the coder claims to have added function X to file Y, open file Y and verify function X is there
40479
+ 3. This 30-second check catches the most common failure mode: coder reports completion but didn't actually make the change
40480
+
40470
40481
  \u2500\u2500 STAGE B: AGENT REVIEW GATES \u2500\u2500
40471
40482
  {{AGENT_PREFIX}}reviewer \u2192 security reviewer (conditional) \u2192 {{AGENT_PREFIX}}test_engineer verification \u2192 {{AGENT_PREFIX}}test_engineer adversarial \u2192 coverage check
40472
40483
  Stage B CANNOT be skipped for TIER 1-3 classifications. Stage A passing does not satisfy Stage B.
@@ -41308,10 +41319,26 @@ RULES:
41308
41319
  - Read target file before editing
41309
41320
  - Implement exactly what TASK specifies
41310
41321
  - Respect CONSTRAINT
41311
- - No research, no web searches, no documentation lookups
41312
- - Use training knowledge for APIs
41322
+ - No web searches or documentation lookups \u2014 but DO search the codebase with grep/glob before using any function
41323
+ - Verify all import paths exist before using them
41324
+
41325
+ ## ANTI-HALLUCINATION PROTOCOL (MANDATORY)
41326
+ Before importing ANY function, type, or class from an existing project module:
41327
+ 1. Run grep to find the exact export: grep -rn "export.*functionName" src/
41328
+ 2. Read the file that contains the export to verify its signature
41329
+ 3. Use the EXACT function name and import path you found \u2014 do not guess or abbreviate
41330
+
41331
+ If grep returns zero results, the function does not exist. Do NOT:
41332
+ - Import it anyway hoping it exists somewhere
41333
+ - Create a similar-sounding function name
41334
+ - Assume an export exists based on naming conventions
41335
+
41336
+ WRONG: import { saveEvidence } from '../evidence/manager' (guessed path)
41337
+ RIGHT: [grep first, then] import { saveEvidence } from '../evidence/manager' (verified path)
41313
41338
 
41314
- ## DEFENSIVE CODING RULES
41339
+ If available_symbols was provided in your scope declaration, you MUST only call functions from that list when importing from existing project modules. Do not invent function names that are not in the list.
41340
+
41341
+ ## DEFENSIVE CODING RULES
41315
41342
  - NEVER use \`any\` type in TypeScript \u2014 always use specific types
41316
41343
  - NEVER leave empty catch blocks \u2014 at minimum log the error
41317
41344
  - NEVER use string concatenation for paths \u2014 use \`path.join()\` or \`path.resolve()\`
@@ -41329,6 +41356,12 @@ RULES:
41329
41356
  - Avoid shell commands in code \u2014 use Node.js APIs (\`fs\`, \`child_process\` with \`shell: false\`)
41330
41357
  - Consider case-sensitivity: Linux filesystems are case-sensitive; Windows and macOS are not
41331
41358
 
41359
+ ## TEST FRAMEWORK
41360
+ - Import from 'bun:test', NOT from 'vitest'. The APIs are identical but the import source matters.
41361
+ - Use: import { describe, test, expect, vi, mock, beforeEach, afterEach } from 'bun:test'
41362
+ - vi.mock() must be at the top level of the file, BEFORE importing the mocked module
41363
+ - mock.module() is the Bun-native equivalent of vi.mock() \u2014 prefer it for new code
41364
+
41332
41365
  ## ERROR HANDLING
41333
41366
  When your implementation encounters an error or unexpected state:
41334
41367
  1. DO NOT silently swallow errors
@@ -41968,6 +42001,13 @@ WORKFLOW:
41968
42001
  - Inline comments explaining obvious code (code should be self-documenting)
41969
42002
  - TODO comments in code (those go through the task system, not code comments)
41970
42003
 
42004
+ ## RELEASE NOTES
42005
+ When writing release notes (docs/releases/v{VERSION}.md):
42006
+ - Determine next version from .release-please-manifest.json + commit type (feat \u2192 minor, fix \u2192 patch)
42007
+ - Follow the established format in existing release notes files
42008
+ - Include: overview, breaking changes (if any), new features, bug fixes, internal improvements
42009
+ - Do NOT manually edit package.json version, CHANGELOG.md, or .release-please-manifest.json \u2014 release-please owns these
42010
+
41971
42011
  ## QUALITY RULES
41972
42012
  - Code examples in docs MUST be syntactically valid \u2014 test them mentally against the actual code
41973
42013
  - API examples MUST show both a success case AND an error/edge case
@@ -41984,6 +42024,11 @@ RULES:
41984
42024
  - No fabrication: if you cannot determine behavior from the code, say so explicitly
41985
42025
  - Update version references if package.json version changed
41986
42026
 
42027
+ ## DOCUMENTATION RULES
42028
+ - Do NOT auto-generate CLAUDE.md or AGENTS.md content \u2014 research shows this hurts agent performance
42029
+ - When updating architecture.md, add new tools/hooks/agents but do not rewrite existing descriptions
42030
+ - When updating README.md, keep the Performance section near the top (after Quick Start)
42031
+
41987
42032
  OUTPUT FORMAT (MANDATORY \u2014 deviations will be rejected):
41988
42033
  Begin directly with UPDATED. Do NOT prepend "Here's what I updated..." or any conversational preamble.
41989
42034
 
@@ -42100,6 +42145,36 @@ COMPATIBLE_CHANGES: [list, or "none"]
42100
42145
  CONSUMERS_AFFECTED: [list of files that import/use changed exports, or "none"]
42101
42146
  VERDICT: BREAKING | COMPATIBLE
42102
42147
  MIGRATION_NEEDED: [yes \u2014 description of required caller updates | no]
42148
+
42149
+ ## DOCUMENTATION DISCOVERY MODE
42150
+ Activates automatically during codebase reality check at plan ingestion.
42151
+
42152
+ STEPS:
42153
+ 1. Glob for documentation files:
42154
+ - Root: README.md, CONTRIBUTING.md, CHANGELOG.md, ARCHITECTURE.md, CLAUDE.md, AGENTS.md, .github/*.md
42155
+ - docs/**/*.md, doc/**/*.md (one level deep only)
42156
+
42157
+ 2. For each file found, read the first 30 lines. Extract:
42158
+ - path: relative to project root
42159
+ - title: first # heading, or filename if no heading
42160
+ - summary: first non-empty paragraph after the title (max 200 chars, use the ACTUAL text, do NOT summarize with your own words)
42161
+ - lines: total line count
42162
+ - mtime: file modification timestamp
42163
+
42164
+ 3. Write manifest to .swarm/doc-manifest.json:
42165
+ { "schema_version": 1, "scanned_at": "ISO timestamp", "files": [...] }
42166
+
42167
+ 4. For each file in the manifest, check relevance to the current plan:
42168
+ - Score by keyword overlap: do any task file paths or directory names appear in the doc's path or summary?
42169
+ - For files scoring > 0, read the full content and extract up to 5 actionable constraints per doc (max 200 chars each)
42170
+ - Write constraints to .swarm/knowledge/doc-constraints.jsonl as knowledge entries with source: "doc-scan", category: "architecture"
42171
+
42172
+ 5. Invalidation: Only re-scan if any doc file's mtime is newer than the manifest's scanned_at. Otherwise reuse the cached manifest.
42173
+
42174
+ RULES:
42175
+ - The manifest must be small (<100 lines). Pointers only, not full content.
42176
+ - Do NOT rephrase or summarize doc content with your own words \u2014 use the actual text from the file
42177
+ - Full doc content is only loaded when relevant to the current task, never preloaded
42103
42178
  `;
42104
42179
  function createExplorerAgent(model, customPrompt, customAppendPrompt) {
42105
42180
  let prompt = EXPLORER_PROMPT;
@@ -42164,6 +42239,12 @@ DO NOT:
42164
42239
 
42165
42240
  Your unique value is catching LOGIC ERRORS, EDGE CASES, and SECURITY FLAWS that automated tools cannot detect. If your review only catches things a linter would catch, you are not adding value.
42166
42241
 
42242
+ DO (explicitly):
42243
+ - READ the changed files yourself \u2014 do not rely on the coder's self-report
42244
+ - VERIFY imports exist: if the coder added a new import, grep for the export in the source
42245
+ - CHECK test files were updated: if the coder changed a function signature, the tests should reflect it
42246
+ - VERIFY platform compatibility: path.join() used for all paths, no hardcoded separators
42247
+
42167
42248
  ## REVIEW REASONING
42168
42249
  For each changed function or method, answer these before formulating issues:
42169
42250
  1. PRECONDITIONS: What must be true for this code to work correctly?
@@ -42193,6 +42274,15 @@ Does the code do what the task acceptance criteria require? Check: every accepta
42193
42274
  TIER 2: SAFETY (mandatory for MODERATE+, always for COMPLEX)
42194
42275
  Does the code introduce security vulnerabilities, data loss risks, or breaking changes? Check against: SAST findings, secret scan results, import analysis. Anti-rubber-stamp: "No issues found" requires evidence. State what you checked.
42195
42276
 
42277
+ ### SAST TRIAGE (within Tier 2)
42278
+ When SAST findings are included in your review input (via GATES field):
42279
+ For each finding, evaluate whether the flagged taint path is actually exploitable:
42280
+ - If a sanitizer, validator, or type guard exists between source and sink \u2192 DISMISS as false positive
42281
+ - If the taint path crosses a trust boundary without validation \u2192 ESCALATE as true positive
42282
+ - If the finding is in test code or mock setup \u2192 DISMISS
42283
+ Report: "SAST TRIAGE: N findings reviewed, M dismissed (false positive), K escalated"
42284
+ Do not rubber-stamp all findings as issues. Do not dismiss all findings without reading the code path.
42285
+
42196
42286
  TIER 3: QUALITY (run only for COMPLEX, and only if Tiers 1-2 pass)
42197
42287
  Code style, naming, duplication, test coverage, documentation completeness. This tier is advisory \u2014 QUALITY findings do not block approval. Approval requires: Tier 1 PASS + Tier 2 PASS (where applicable). Tier 3 is informational. Flag these slop patterns:
42198
42288
  - Vague identifiers (result, data, temp, value, item, info, stuff, obj, ret, val) \u2014 flag if a more descriptive name exists
@@ -42432,7 +42522,10 @@ COVERAGE:
42432
42522
  - Errors: invalid inputs, failures
42433
42523
 
42434
42524
  RULES:
42435
- - Match language (PowerShell \u2192 Pester, Python \u2192 pytest, TS \u2192 vitest/jest)
42525
+ - Match language (PowerShell \u2192 Pester, Python \u2192 pytest, TS \u2192 bun:test)
42526
+ - Import from 'bun:test', NOT from 'vitest': import { describe, test, expect, vi, mock, beforeEach, afterEach } from 'bun:test'
42527
+ - vi.mock() calls MUST be at the top level of the file, BEFORE importing the mocked module
42528
+ - Tests MUST clean up temp directories in afterEach \u2014 leaked dirs break Windows CI
42436
42529
  - Tests must be runnable
42437
42530
  - Include setup/teardown if needed
42438
42531
 
@@ -42534,6 +42627,13 @@ When writing adversarial or security-focused tests, cover these attack categorie
42534
42627
 
42535
42628
  For each adversarial test: assert a SPECIFIC outcome (error thrown, value rejected, sanitized output) \u2014 not just "it doesn't crash."
42536
42629
 
42630
+ ## MOCK ISOLATION RULES
42631
+ - vi.mock() and mock.module() calls persist across tests in the same bun process
42632
+ - Each test file runs in the same process as other files in its CI group
42633
+ - If your mock leaks, it will break other test files \u2014 this is the #1 CI failure cause
42634
+ - ALWAYS call vi.clearAllMocks() or vi.restoreAllMocks() in afterEach
42635
+ - If mocking a module, place the mock BEFORE any import of that module
42636
+
42537
42637
  ## EXECUTION VERIFICATION
42538
42638
 
42539
42639
  After writing tests, you MUST run them. A test file that was written but never executed is NOT a deliverable.
@@ -42598,6 +42698,8 @@ ${customAppendPrompt}`;
42598
42698
  }
42599
42699
 
42600
42700
  // src/agents/index.ts
42701
+ var warnedAgents = new Set;
42702
+ var _swarmAgents;
42601
42703
  function stripSwarmPrefix(agentName, swarmPrefix) {
42602
42704
  if (!swarmPrefix || !agentName)
42603
42705
  return agentName;
@@ -42612,7 +42714,23 @@ function getModelForAgent(agentName, swarmAgents, swarmPrefix) {
42612
42714
  const explicit = swarmAgents?.[baseAgentName]?.model;
42613
42715
  if (explicit)
42614
42716
  return explicit;
42615
- return DEFAULT_MODELS[baseAgentName] ?? DEFAULT_MODELS.default;
42717
+ const resolvedModel = DEFAULT_MODELS[baseAgentName] ?? DEFAULT_MODELS.default;
42718
+ if (!warnedAgents.has(baseAgentName)) {
42719
+ warnedAgents.add(baseAgentName);
42720
+ console.warn("[swarm] Agent '%s' not found in config \u2014 using default model '%s'. Add it to opencode-swarm.json to customize.", baseAgentName, resolvedModel);
42721
+ }
42722
+ return resolvedModel;
42723
+ }
42724
+ function resolveFallbackModel(agentBaseName, fallbackIndex, swarmAgents) {
42725
+ const fallbackModels = swarmAgents?.[agentBaseName]?.fallback_models;
42726
+ if (!fallbackModels || fallbackModels.length === 0)
42727
+ return null;
42728
+ if (fallbackIndex < 1 || fallbackIndex > fallbackModels.length)
42729
+ return null;
42730
+ return fallbackModels[fallbackIndex - 1];
42731
+ }
42732
+ function getSwarmAgents() {
42733
+ return _swarmAgents;
42616
42734
  }
42617
42735
  function isAgentDisabled(agentName, swarmAgents, swarmPrefix) {
42618
42736
  const baseAgentName = stripSwarmPrefix(agentName, swarmPrefix);
@@ -42632,6 +42750,7 @@ function applyOverrides(agent, swarmAgents, swarmPrefix) {
42632
42750
  function createSwarmAgents(swarmId, swarmConfig, isDefault, pluginConfig) {
42633
42751
  const agents = [];
42634
42752
  const swarmAgents = swarmConfig.agents;
42753
+ _swarmAgents = swarmAgents;
42635
42754
  const prefix = isDefault ? "" : `${swarmId}_`;
42636
42755
  const swarmPrefix = isDefault ? undefined : swarmId;
42637
42756
  const qaRetryLimit = pluginConfig?.qa_retry_limit ?? 3;
@@ -42696,12 +42815,12 @@ If you call @coder instead of @${swarmId}_coder, the call will FAIL or go to the
42696
42815
  agents.push(applyOverrides(critic, swarmAgents, swarmPrefix));
42697
42816
  }
42698
42817
  if (!isAgentDisabled("critic_sounding_board", swarmAgents, swarmPrefix)) {
42699
- const critic = createCriticAgent(getModel("critic_sounding_board"), undefined, undefined, "sounding_board");
42818
+ const critic = createCriticAgent(swarmAgents?.["critic_sounding_board"]?.model ?? getModel("critic"), undefined, undefined, "sounding_board");
42700
42819
  critic.name = prefixName("critic_sounding_board");
42701
42820
  agents.push(applyOverrides(critic, swarmAgents, swarmPrefix));
42702
42821
  }
42703
42822
  if (!isAgentDisabled("critic_drift_verifier", swarmAgents, swarmPrefix)) {
42704
- const critic = createCriticAgent(getModel("critic_drift_verifier"), undefined, undefined, "phase_drift_verifier");
42823
+ const critic = createCriticAgent(swarmAgents?.["critic_drift_verifier"]?.model ?? getModel("critic"), undefined, undefined, "phase_drift_verifier");
42705
42824
  critic.name = prefixName("critic_drift_verifier");
42706
42825
  agents.push(applyOverrides(critic, swarmAgents, swarmPrefix));
42707
42826
  }
@@ -50195,9 +50314,9 @@ import * as path32 from "path";
50195
50314
  init_telemetry();
50196
50315
 
50197
50316
  // src/hooks/guardrails.ts
50317
+ import * as path30 from "path";
50198
50318
  init_constants();
50199
50319
  init_schema();
50200
- import * as path30 from "path";
50201
50320
  init_manager2();
50202
50321
  init_telemetry();
50203
50322
  init_utils();
@@ -50401,6 +50520,82 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
50401
50520
  "pre_check_batch"
50402
50521
  ];
50403
50522
  const requireReviewerAndTestEngineer = cfg.qa_gates?.require_reviewer_test_engineer ?? true;
50523
+ async function checkGateLimits(params) {
50524
+ const { sessionID, window: window2, agentConfig, elapsedMinutes, repetitionCount } = params;
50525
+ if (agentConfig.max_tool_calls > 0 && window2.toolCalls >= agentConfig.max_tool_calls) {
50526
+ window2.hardLimitHit = true;
50527
+ telemetry.hardLimitHit(sessionID, window2.agentName, "tool_calls", window2.toolCalls);
50528
+ warn("Circuit breaker: tool call limit hit", {
50529
+ sessionID,
50530
+ agentName: window2.agentName,
50531
+ invocationId: window2.id,
50532
+ windowKey: `${window2.agentName}:${window2.id}`,
50533
+ resolvedMaxCalls: agentConfig.max_tool_calls,
50534
+ currentCalls: window2.toolCalls
50535
+ });
50536
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: Tool calls exhausted (${window2.toolCalls}/${agentConfig.max_tool_calls}). Finish the current operation and return your progress summary.`);
50537
+ }
50538
+ if (agentConfig.max_duration_minutes > 0 && elapsedMinutes >= agentConfig.max_duration_minutes) {
50539
+ window2.hardLimitHit = true;
50540
+ telemetry.hardLimitHit(sessionID, window2.agentName, "duration", elapsedMinutes);
50541
+ warn("Circuit breaker: duration limit hit", {
50542
+ sessionID,
50543
+ agentName: window2.agentName,
50544
+ invocationId: window2.id,
50545
+ windowKey: `${window2.agentName}:${window2.id}`,
50546
+ resolvedMaxMinutes: agentConfig.max_duration_minutes,
50547
+ elapsedMinutes: Math.floor(elapsedMinutes)
50548
+ });
50549
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: Duration exhausted (${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min). Finish the current operation and return your progress summary.`);
50550
+ }
50551
+ if (repetitionCount >= agentConfig.max_repetitions) {
50552
+ window2.hardLimitHit = true;
50553
+ telemetry.hardLimitHit(sessionID, window2.agentName, "repetition", repetitionCount);
50554
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: Repeated the same tool call ${repetitionCount} times. This suggests a loop. Return your progress summary.`);
50555
+ }
50556
+ if (window2.consecutiveErrors >= agentConfig.max_consecutive_errors) {
50557
+ window2.hardLimitHit = true;
50558
+ telemetry.hardLimitHit(sessionID, window2.agentName, "consecutive_errors", window2.consecutiveErrors);
50559
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${window2.consecutiveErrors} consecutive tool errors detected. Return your progress summary with details of what went wrong.`);
50560
+ }
50561
+ const idleMinutes = (Date.now() - window2.lastSuccessTimeMs) / 60000;
50562
+ if (idleMinutes >= agentConfig.idle_timeout_minutes) {
50563
+ window2.hardLimitHit = true;
50564
+ telemetry.hardLimitHit(sessionID, window2.agentName, "idle_timeout", idleMinutes);
50565
+ warn("Circuit breaker: idle timeout hit", {
50566
+ sessionID,
50567
+ agentName: window2.agentName,
50568
+ invocationId: window2.id,
50569
+ windowKey: `${window2.agentName}:${window2.id}`,
50570
+ idleTimeoutMinutes: agentConfig.idle_timeout_minutes,
50571
+ idleMinutes: Math.floor(idleMinutes)
50572
+ });
50573
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: No successful tool call for ${Math.floor(idleMinutes)} minutes (idle timeout: ${agentConfig.idle_timeout_minutes} min). This suggests the agent may be stuck. Return your progress summary.`);
50574
+ }
50575
+ if (!window2.warningIssued) {
50576
+ const toolPct = agentConfig.max_tool_calls > 0 ? window2.toolCalls / agentConfig.max_tool_calls : 0;
50577
+ const durationPct = agentConfig.max_duration_minutes > 0 ? elapsedMinutes / agentConfig.max_duration_minutes : 0;
50578
+ const repPct = repetitionCount / agentConfig.max_repetitions;
50579
+ const errorPct = window2.consecutiveErrors / agentConfig.max_consecutive_errors;
50580
+ const reasons = [];
50581
+ if (agentConfig.max_tool_calls > 0 && toolPct >= agentConfig.warning_threshold) {
50582
+ reasons.push(`tool calls ${window2.toolCalls}/${agentConfig.max_tool_calls}`);
50583
+ }
50584
+ if (durationPct >= agentConfig.warning_threshold) {
50585
+ reasons.push(`duration ${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min`);
50586
+ }
50587
+ if (repPct >= agentConfig.warning_threshold) {
50588
+ reasons.push(`repetitions ${repetitionCount}/${agentConfig.max_repetitions}`);
50589
+ }
50590
+ if (errorPct >= agentConfig.warning_threshold) {
50591
+ reasons.push(`errors ${window2.consecutiveErrors}/${agentConfig.max_consecutive_errors}`);
50592
+ }
50593
+ if (reasons.length > 0) {
50594
+ window2.warningIssued = true;
50595
+ window2.warningReason = reasons.join(", ");
50596
+ }
50597
+ }
50598
+ }
50404
50599
  return {
50405
50600
  toolBefore: async (input, output) => {
50406
50601
  const currentSession = swarmState.agentSessions.get(input.sessionID);
@@ -50637,79 +50832,13 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
50637
50832
  }
50638
50833
  }
50639
50834
  const elapsedMinutes = (Date.now() - window2.startedAtMs) / 60000;
50640
- if (agentConfig.max_tool_calls > 0 && window2.toolCalls >= agentConfig.max_tool_calls) {
50641
- window2.hardLimitHit = true;
50642
- telemetry.hardLimitHit(input.sessionID, window2.agentName, "tool_calls", window2.toolCalls);
50643
- warn("Circuit breaker: tool call limit hit", {
50644
- sessionID: input.sessionID,
50645
- agentName: window2.agentName,
50646
- invocationId: window2.id,
50647
- windowKey: `${window2.agentName}:${window2.id}`,
50648
- resolvedMaxCalls: agentConfig.max_tool_calls,
50649
- currentCalls: window2.toolCalls
50650
- });
50651
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: Tool calls exhausted (${window2.toolCalls}/${agentConfig.max_tool_calls}). Finish the current operation and return your progress summary.`);
50652
- }
50653
- if (agentConfig.max_duration_minutes > 0 && elapsedMinutes >= agentConfig.max_duration_minutes) {
50654
- window2.hardLimitHit = true;
50655
- telemetry.hardLimitHit(input.sessionID, window2.agentName, "duration", elapsedMinutes);
50656
- warn("Circuit breaker: duration limit hit", {
50657
- sessionID: input.sessionID,
50658
- agentName: window2.agentName,
50659
- invocationId: window2.id,
50660
- windowKey: `${window2.agentName}:${window2.id}`,
50661
- resolvedMaxMinutes: agentConfig.max_duration_minutes,
50662
- elapsedMinutes: Math.floor(elapsedMinutes)
50663
- });
50664
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: Duration exhausted (${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min). Finish the current operation and return your progress summary.`);
50665
- }
50666
- if (repetitionCount >= agentConfig.max_repetitions) {
50667
- window2.hardLimitHit = true;
50668
- telemetry.hardLimitHit(input.sessionID, window2.agentName, "repetition", repetitionCount);
50669
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: Repeated the same tool call ${repetitionCount} times. This suggests a loop. Return your progress summary.`);
50670
- }
50671
- if (window2.consecutiveErrors >= agentConfig.max_consecutive_errors) {
50672
- window2.hardLimitHit = true;
50673
- telemetry.hardLimitHit(input.sessionID, window2.agentName, "consecutive_errors", window2.consecutiveErrors);
50674
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${window2.consecutiveErrors} consecutive tool errors detected. Return your progress summary with details of what went wrong.`);
50675
- }
50676
- const idleMinutes = (Date.now() - window2.lastSuccessTimeMs) / 60000;
50677
- if (idleMinutes >= agentConfig.idle_timeout_minutes) {
50678
- window2.hardLimitHit = true;
50679
- telemetry.hardLimitHit(input.sessionID, window2.agentName, "idle_timeout", idleMinutes);
50680
- warn("Circuit breaker: idle timeout hit", {
50681
- sessionID: input.sessionID,
50682
- agentName: window2.agentName,
50683
- invocationId: window2.id,
50684
- windowKey: `${window2.agentName}:${window2.id}`,
50685
- idleTimeoutMinutes: agentConfig.idle_timeout_minutes,
50686
- idleMinutes: Math.floor(idleMinutes)
50687
- });
50688
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: No successful tool call for ${Math.floor(idleMinutes)} minutes (idle timeout: ${agentConfig.idle_timeout_minutes} min). This suggests the agent may be stuck. Return your progress summary.`);
50689
- }
50690
- if (!window2.warningIssued) {
50691
- const toolPct = agentConfig.max_tool_calls > 0 ? window2.toolCalls / agentConfig.max_tool_calls : 0;
50692
- const durationPct = agentConfig.max_duration_minutes > 0 ? elapsedMinutes / agentConfig.max_duration_minutes : 0;
50693
- const repPct = repetitionCount / agentConfig.max_repetitions;
50694
- const errorPct = window2.consecutiveErrors / agentConfig.max_consecutive_errors;
50695
- const reasons = [];
50696
- if (agentConfig.max_tool_calls > 0 && toolPct >= agentConfig.warning_threshold) {
50697
- reasons.push(`tool calls ${window2.toolCalls}/${agentConfig.max_tool_calls}`);
50698
- }
50699
- if (durationPct >= agentConfig.warning_threshold) {
50700
- reasons.push(`duration ${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min`);
50701
- }
50702
- if (repPct >= agentConfig.warning_threshold) {
50703
- reasons.push(`repetitions ${repetitionCount}/${agentConfig.max_repetitions}`);
50704
- }
50705
- if (errorPct >= agentConfig.warning_threshold) {
50706
- reasons.push(`errors ${window2.consecutiveErrors}/${agentConfig.max_consecutive_errors}`);
50707
- }
50708
- if (reasons.length > 0) {
50709
- window2.warningIssued = true;
50710
- window2.warningReason = reasons.join(", ");
50711
- }
50712
- }
50835
+ await checkGateLimits({
50836
+ sessionID: input.sessionID,
50837
+ window: window2,
50838
+ agentConfig,
50839
+ elapsedMinutes,
50840
+ repetitionCount
50841
+ });
50713
50842
  setStoredInputArgs(input.callID, output.args);
50714
50843
  },
50715
50844
  toolAfter: async (input, output) => {
@@ -50797,6 +50926,7 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
50797
50926
  const normalizedToolName = input.tool.replace(/^[^:]+[:.]/, "");
50798
50927
  if (isWriteTool(normalizedToolName)) {
50799
50928
  toolCallsSinceLastWrite.set(sessionId, 0);
50929
+ noOpWarningIssued.delete(sessionId);
50800
50930
  } else {
50801
50931
  const count = (toolCallsSinceLastWrite.get(sessionId) ?? 0) + 1;
50802
50932
  toolCallsSinceLastWrite.set(sessionId, count);
@@ -50817,10 +50947,17 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
50817
50947
  const errorContent = output.error ?? outputStr;
50818
50948
  if (typeof errorContent === "string" && TRANSIENT_MODEL_ERROR_PATTERN.test(errorContent) && !session.modelFallbackExhausted) {
50819
50949
  session.model_fallback_index++;
50820
- telemetry.modelFallback(input.sessionID, session.agentName, "primary", "fallback", "transient_model_error");
50821
50950
  session.modelFallbackExhausted = true;
50822
- session.pendingAdvisoryMessages ??= [];
50823
- session.pendingAdvisoryMessages.push(`MODEL FALLBACK: Transient model error detected (attempt ${session.model_fallback_index}). ` + `The agent model may be rate-limited, overloaded, or temporarily unavailable. ` + `Consider retrying with a fallback model or waiting before retrying.`);
50951
+ const baseAgentName = session.agentName ? session.agentName.replace(/^[^_]+[_]/, "") : "";
50952
+ const fallbackModel = resolveFallbackModel(baseAgentName, session.model_fallback_index, getSwarmAgents());
50953
+ if (fallbackModel) {
50954
+ telemetry.modelFallback(input.sessionID, session.agentName, "primary", fallbackModel, "transient_model_error");
50955
+ session.pendingAdvisoryMessages ??= [];
50956
+ session.pendingAdvisoryMessages.push(`MODEL FALLBACK: Transient model error detected (attempt ${session.model_fallback_index}). ` + `Configured fallback model: "${fallbackModel}". ` + `Consider retrying with this model or using /swarm handoff to reset.`);
50957
+ } else {
50958
+ session.pendingAdvisoryMessages ??= [];
50959
+ session.pendingAdvisoryMessages.push(`MODEL FALLBACK: Transient model error detected (attempt ${session.model_fallback_index}). ` + `No fallback models configured for this agent. Add "fallback_models": ["model-a", "model-b"] ` + `to the agent's config in opencode-swarm.json.`);
50960
+ }
50824
50961
  swarmState.pendingEvents++;
50825
50962
  }
50826
50963
  }
@@ -56754,7 +56891,7 @@ function matchesDocPattern(filePath, patterns) {
56754
56891
  }
56755
56892
  const patternNormalized = normalizeSeparators(pattern);
56756
56893
  const dirPrefix = patternNormalized.replace(/\/\*\*.*$/, "").replace(/\/\*.*$/, "");
56757
- if (normalizedPath.startsWith(dirPrefix + "/") || normalizedPath === dirPrefix) {
56894
+ if (normalizedPath.startsWith(`${dirPrefix}/`) || normalizedPath === dirPrefix) {
56758
56895
  return true;
56759
56896
  }
56760
56897
  }
@@ -56785,7 +56922,7 @@ function extractTitleAndSummary(content, filename) {
56785
56922
  }
56786
56923
  }
56787
56924
  if (summary.length > MAX_SUMMARY_LENGTH) {
56788
- summary = summary.slice(0, MAX_SUMMARY_LENGTH - 3) + "...";
56925
+ summary = `${summary.slice(0, MAX_SUMMARY_LENGTH - 3)}...`;
56789
56926
  }
56790
56927
  return { title, summary };
56791
56928
  }
@@ -58770,7 +58907,9 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
58770
58907
  message: `Phase ${phase} cannot be completed: ${completionResult.reason}`,
58771
58908
  agentsDispatched,
58772
58909
  agentsMissing: [],
58773
- warnings: completionResult.blockedTasks ? [`Blocked tasks: ${completionResult.blockedTasks.map((t) => t.task_id).join(", ")}`] : []
58910
+ warnings: completionResult.blockedTasks ? [
58911
+ `Blocked tasks: ${completionResult.blockedTasks.map((t) => t.task_id).join(", ")}`
58912
+ ] : []
58774
58913
  }, null, 2);
58775
58914
  }
58776
58915
  } catch (completionError) {
@@ -65945,7 +66084,7 @@ var OpenCodeSwarm = async (ctx) => {
65945
66084
  }
65946
66085
  ].filter((fn) => Boolean(fn))),
65947
66086
  "experimental.chat.system.transform": composeHandlers(...[
65948
- async (input, output) => {
66087
+ async (_input, _output) => {
65949
66088
  if (process.env.DEBUG_SWARM)
65950
66089
  console.error(`[DIAG] systemTransform START`);
65951
66090
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.34.0",
3
+ "version": "6.35.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",