ruvnet/ruflo · error

Cannot remove ${name}: required by ${dependents.join(', ')}

Error message

Cannot remove ${name}: required by ${dependents.join(', ')}

What it means

Thrown by EnhancedPluginRegistry.unregister(name) when other plugins depend on the target (dependencyGraph.getDependents(name) is non-empty) and neither options.cascade nor options.force is set. The registry refuses to remove a plugin that would leave dependents with a broken dependency; cascade instead shuts dependents down first (reverse removal order), and force removes while only logging which links are broken.

Source

Thrown at v3/@claude-flow/plugins/src/registry/enhanced-plugin-registry.ts:370

    }

    // Check dependents
    const dependents = this.dependencyGraph.getDependents(name);

    if (dependents.length > 0) {
      if (options?.cascade) {
        // Unload dependents first (in reverse order)
        const order = this.dependencyGraph.getRemovalOrder(name);
        for (const dep of order) {
          if (dep !== name) {
            await this.shutdownPlugin(dep);
            this.removePluginFromGraph(dep);
          }
        }
      } else if (options?.force) {
        this.logger.warn(`Force removing ${name}, breaking: ${dependents.join(', ')}`);
      } else {
        throw new Error(`Cannot remove ${name}: required by ${dependents.join(', ')}`);
      }
    }

    // Shutdown and remove
    await this.shutdownPlugin(name);
    this.removePluginFromGraph(name);

    this.logger.info(`Plugin unregistered: ${name}`);
  }

  // =========================================================================
  // Initialization
  // =========================================================================

  /**
   * Initialize all registered plugins.
   */
  async initialize(): Promise<void> {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Unregister dependents first (leaf plugins), then the shared plugin
  2. Pass { cascade: true } to remove the plugin together with its dependents in reverse order
  3. Pass { force: true } only when you accept breaking dependents at runtime (registry logs a warning) - e.g. forced shutdown

Example fix

// before
await registry.unregister('core-utils'); // 'my-plugin' depends on it -> throws

// after
await registry.unregister('core-utils', { cascade: true }); // removes dependents too
// or remove leaves first:
await registry.unregister('my-plugin');
await registry.unregister('core-utils');
Defensive patterns

Strategy: fallback

Validate before calling

const dependents = await getDependentsOf(registry, name); // mirror of dependencyGraph.getDependents
if (dependents.length > 0 && !options?.cascade && !options?.force) {
  // remove leaves first instead of removing a required base
  for (const dep of dependents) {
    await registry.unregister(dep);
  }
}
await registry.unregister(name);

Try / catch

try {
  await registry.unregister(name);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Cannot remove')) {
    // rethrow as a typed signal so callers can choose cascade vs leaves-first
    throw new PluginHasDependentsError(name, err.message);
  }
  throw err;
}

Prevention

When it happens

Trigger: unregister('core-utils') while 'my-plugin' declares a dependency on 'core-utils' via metadata.dependencies, without passing options; attempting teardown in the wrong order (shared plugin before its dependents); removing a base plugin during shutdown while feature plugins are still registered.

Common situations: Hot-swapping a shared library plugin that others import; partial unregisters during shutdown sequencing; test fixtures removing a common dependency while dependent plugins remain loaded.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/fd168626167d7bc5. Report an issue: GitHub.