diff --git a/eslint-rules/no-core-root-barrel-import.js b/eslint-rules/no-core-root-barrel-import.js
new file mode 100644
index 00000000000..1b926d58cb8
--- /dev/null
+++ b/eslint-rules/no-core-root-barrel-import.js
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2025 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @fileoverview Prevents core production modules from importing the core root barrel.
+ */
+
+import path from 'node:path';
+
+const TEST_OR_FIXTURE_SEGMENTS = new Set(['__tests__', 'fixtures']);
+
+function isCoreProductionFile(filename) {
+ if (!filename || filename === '' || filename === '') return false;
+ const normalized = path.normalize(filename).replaceAll('\\', '/');
+ const marker = 'packages/core/src/';
+ const start = normalized.indexOf(marker);
+ if (start < 0) return false;
+ const relativePath = normalized.slice(start + marker.length);
+ const segments = relativePath.split('/');
+ return !segments.some((segment) => TEST_OR_FIXTURE_SEGMENTS.has(segment)) &&
+ !/\.test\.[cm]?[jt]sx?$/.test(relativePath);
+}
+
+function resolvesToCoreRootBarrel(filename, importedPath) {
+ if (!importedPath.startsWith('.')) return false;
+ const normalized = path.normalize(filename).replaceAll('\\', '/');
+ const marker = 'packages/core/src/';
+ const sourceRoot = path.resolve(
+ normalized.slice(0, normalized.indexOf(marker) + marker.length),
+ );
+ const resolvedImport = path.resolve(path.dirname(filename), importedPath);
+ const relativeToSource = path.relative(sourceRoot, resolvedImport);
+ return relativeToSource === 'index.js' || relativeToSource === 'index.ts';
+}
+
+export default {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'Disallow core production modules from importing the core root barrel.',
+ },
+ schema: [],
+ messages: {
+ noCoreRootBarrelImport:
+ 'Core production modules must import symbols from their direct owner modules, not ../index.js.',
+ },
+ },
+
+ create(context) {
+ const filename = context.filename;
+ if (!isCoreProductionFile(filename)) {
+ return {};
+ }
+
+ function checkSource(node) {
+ if (
+ node.source &&
+ typeof node.source.value === 'string' &&
+ resolvesToCoreRootBarrel(filename, node.source.value)
+ ) {
+ context.report({
+ node: node.source,
+ messageId: 'noCoreRootBarrelImport',
+ });
+ }
+ }
+
+ return {
+ ImportDeclaration: checkSource,
+ ExportNamedDeclaration: checkSource,
+ ExportAllDeclaration: checkSource,
+ ImportExpression(node) {
+ if (node.source.type === 'Literal') checkSource(node);
+ },
+ };
+ },
+};
diff --git a/eslint.config.js b/eslint.config.js
index 08f87d9ee30..012e3327718 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -15,6 +15,7 @@ import globals from 'globals';
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from 'eslint-plugin-storybook';
import checkFile from 'eslint-plugin-check-file';
+import noCoreRootBarrelImport from './eslint-rules/no-core-root-barrel-import.js';
import { legacyFilenames } from './eslint.legacy-filenames.mjs';
export default tseslint.config(
@@ -174,6 +175,22 @@ export default tseslint.config(
'default-case': 'error',
},
},
+ {
+ files: ['packages/core/src/**/*.{ts,tsx}'],
+ ignores: [
+ 'packages/core/src/**/*.test.{ts,tsx}',
+ 'packages/core/src/**/__tests__/**',
+ 'packages/core/src/**/fixtures/**',
+ ],
+ plugins: {
+ architecture: {
+ rules: { 'no-core-root-barrel-import': noCoreRootBarrelImport },
+ },
+ },
+ rules: {
+ 'architecture/no-core-root-barrel-import': 'error',
+ },
+ },
{
files: [
'packages/web-shell/client/**/*.{ts,tsx}',
diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts
index 54f53613c18..4fa9a8ba2c6 100644
--- a/packages/core/src/core/coreToolScheduler.ts
+++ b/packages/core/src/core/coreToolScheduler.ts
@@ -4,21 +4,20 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import type { ToolCallRequestInfo, ToolCallResponseInfo } from './turn.js';
import type {
- ToolCallRequestInfo,
- ToolCallResponseInfo,
ToolCallConfirmationDetails,
ToolResult,
ToolResultDisplay,
- ToolRegistry,
- EditorType,
- Config,
ToolConfirmationPayload,
AnyDeclarativeTool,
AnyToolInvocation,
- ChatRecordingService,
ToolArtifact,
-} from '../index.js';
+} from '../tools/tools.js';
+import type { EditorType } from '../utils/editor.js';
+import type { Config } from '../config/config.js';
+import type { ToolRegistry } from '../tools/tool-registry.js';
+import type { ChatRecordingService } from '../services/chatRecordingService.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import { sanitizeToolNameForProvider } from '../utils/tool-name-utils.js';
import { compactToolResultDisplayForHistory } from '../utils/toolResultDisplayCompaction.js';
@@ -44,15 +43,12 @@ import {
finalizeToolResponses,
toolResponseTextLength,
} from '../utils/tool-response-finalizer.js';
-import {
- ToolConfirmationOutcome,
- ApprovalMode,
- logToolCall,
- ToolErrorType,
- ToolCallEvent,
- InputFormat,
- Kind,
-} from '../index.js';
+import { ToolConfirmationOutcome, Kind } from '../tools/tools.js';
+import { ApprovalMode } from '../config/approval-mode.js';
+import { logToolCall } from '../telemetry/loggers.js';
+import { ToolCallEvent } from '../telemetry/types.js';
+import { InputFormat } from '../output/types.js';
+import { ToolErrorType } from '../tools/tool-error.js';
import type {
FunctionResponse,
FunctionResponsePart,
diff --git a/packages/core/src/core/nonInteractiveToolExecutor.ts b/packages/core/src/core/nonInteractiveToolExecutor.ts
index 06393c29f56..f0dbf769b79 100644
--- a/packages/core/src/core/nonInteractiveToolExecutor.ts
+++ b/packages/core/src/core/nonInteractiveToolExecutor.ts
@@ -4,11 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {
- ToolCallRequestInfo,
- ToolCallResponseInfo,
- Config,
-} from '../index.js';
+import type { ToolCallRequestInfo, ToolCallResponseInfo } from './turn.js';
+import type { Config } from '../config/config.js';
import {
CoreToolScheduler,
type AllToolCallsCompleteHandler,
diff --git a/packages/core/src/core/permissionFlow.ts b/packages/core/src/core/permissionFlow.ts
index a1a9cafdafa..f2cb3dbb656 100644
--- a/packages/core/src/core/permissionFlow.ts
+++ b/packages/core/src/core/permissionFlow.ts
@@ -19,8 +19,10 @@
* `invocation.getConfirmationDetails()`.
*/
-import type { AnyToolInvocation, Config } from '../index.js';
-import { ApprovalMode, ToolNames } from '../index.js';
+import type { AnyToolInvocation } from '../tools/tools.js';
+import type { Config } from '../config/config.js';
+import { ApprovalMode } from '../config/approval-mode.js';
+import { ToolNames } from '../tools/tool-names.js';
import {
buildPermissionCheckContext,
evaluatePermissionRules,
diff --git a/packages/core/src/extension/extensionManager.test.ts b/packages/core/src/extension/extensionManager.test.ts
index 29fe55f582f..73c3c219c2b 100644
--- a/packages/core/src/extension/extensionManager.test.ts
+++ b/packages/core/src/extension/extensionManager.test.ts
@@ -81,6 +81,9 @@ const mockLogExtensionDisable = vi.hoisted(() => vi.fn());
const mockLogExtensionUpdateEvent = vi.hoisted(() => vi.fn());
vi.mock('../telemetry/loggers.js', () => ({
logExtensionEnable: mockLogExtensionEnable,
+ logExtensionInstallEvent: mockLogExtensionInstallEvent,
+ logExtensionUninstall: mockLogExtensionUninstall,
+ logExtensionDisable: mockLogExtensionDisable,
logExtensionUpdateEvent: mockLogExtensionUpdateEvent,
}));
diff --git a/packages/core/src/extension/extensionManager.ts b/packages/core/src/extension/extensionManager.ts
index 05ecb4bfbd6..f82bc0c60cb 100644
--- a/packages/core/src/extension/extensionManager.ts
+++ b/packages/core/src/extension/extensionManager.ts
@@ -4,22 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {
- MCPServerConfig,
- ExtensionInstallMetadata,
- SkillConfig,
- SubagentConfig,
- ClaudeMarketplaceConfig,
-} from '../index.js';
+import type { MCPServerConfig, ExtensionInstallMetadata } from '../config/config.js';
+import { Config } from '../config/config.js';
+import type { SkillConfig } from '../skills/types.js';
+import type { SubagentConfig } from '../subagents/types.js';
+import type { ClaudeMarketplaceConfig } from './claude-converter.js';
import type { HookEventName, HookDefinition } from '../hooks/types.js';
+import { Storage } from '../config/storage.js';
import {
- Storage,
- Config,
logExtensionEnable,
logExtensionInstallEvent,
logExtensionUninstall,
logExtensionDisable,
-} from '../index.js';
+} from '../telemetry/loggers.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts
index 8487f178253..9b51e6bc95f 100644
--- a/packages/core/src/utils/shell-utils.ts
+++ b/packages/core/src/utils/shell-utils.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type { AnyToolInvocation } from '../index.js';
+import type { AnyToolInvocation } from '../tools/tools.js';
import type { Config } from '../config/config.js';
import os from 'node:os';
import path from 'node:path';
diff --git a/packages/core/src/utils/tool-utils.ts b/packages/core/src/utils/tool-utils.ts
index 251b00f3e50..836c9898c1e 100644
--- a/packages/core/src/utils/tool-utils.ts
+++ b/packages/core/src/utils/tool-utils.ts
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type { AnyDeclarativeTool, AnyToolInvocation } from '../index.js';
-import { isTool } from '../index.js';
+import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js';
+import { isTool } from '../tools/tools.js';
import {
ToolNames,
ToolDisplayNames,
diff --git a/scripts/tests/no-core-root-barrel-config.test.js b/scripts/tests/no-core-root-barrel-config.test.js
new file mode 100644
index 00000000000..e69d80cb305
--- /dev/null
+++ b/scripts/tests/no-core-root-barrel-config.test.js
@@ -0,0 +1,27 @@
+import { describe, expect, it } from 'vitest';
+import { ESLint } from 'eslint';
+
+describe('core root barrel flat-config integration', () => {
+ it('reports a production self-import and ignores tests', async () => {
+ const eslint = new ESLint({ cwd: process.cwd(), overrideConfigFile: 'eslint.config.js' });
+ const [production, test] = await eslint.lintText(
+ "import value from '../index.js';",
+ { filePath: 'packages/core/src/core/fixture-boundary.ts' },
+ ).then(async (results) => [
+ results,
+ eslint.lintText("import value from '../index.js';", {
+ filePath: 'packages/core/src/core/fixture-boundary.test.ts',
+ }),
+ ]);
+ expect(
+ production[0].messages.some(
+ (message) => message.ruleId === 'architecture/no-core-root-barrel-import',
+ ),
+ ).toBe(true);
+ expect(
+ (await test).some((result) =>
+ result.messages.some((message) => message.ruleId === 'architecture/no-core-root-barrel-import'),
+ ),
+ ).toBe(false);
+ });
+});
diff --git a/scripts/tests/no-core-root-barrel-import.test.js b/scripts/tests/no-core-root-barrel-import.test.js
new file mode 100644
index 00000000000..a60b196fa75
--- /dev/null
+++ b/scripts/tests/no-core-root-barrel-import.test.js
@@ -0,0 +1,42 @@
+import { describe, expect, it } from 'vitest';
+import { Linter } from 'eslint';
+import rule from '../../eslint-rules/no-core-root-barrel-import.js';
+
+function runRule(code, filename) {
+ const linter = new Linter({ configType: 'eslintrc' });
+ linter.defineRule('architecture/no-core-root-barrel-import', rule);
+ return linter.verify(
+ code,
+ {
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
+ rules: { 'architecture/no-core-root-barrel-import': 'error' },
+ },
+ { filename },
+ );
+}
+
+describe('no-core-root-barrel-import', () => {
+ it.each([
+ ['packages/core/src/core/client.ts', '../index.js'],
+ ['packages/core/src/core/deep/module.ts', '../../index.js'],
+ ['packages/core/src/a/b/c/module.ts', '../../../index.js'],
+ ])('rejects root barrel imports at depth %s', (filename, importedPath) => {
+ expect(runRule(`import value from '${importedPath}';`, filename)).toHaveLength(1);
+ });
+
+ it('rejects export and dynamic root barrel sources', () => {
+ expect(runRule("export { value } from '../index.js';", 'packages/core/src/core/client.ts')).toHaveLength(1);
+ expect(runRule("export * from '../index.js';", 'packages/core/src/core/client.ts')).toHaveLength(1);
+ expect(runRule("import('../index.js');", 'packages/core/src/core/client.ts')).toHaveLength(1);
+ });
+
+ it('allows tests, fixtures, and non-core consumers', () => {
+ expect(runRule("import value from '../index.js';", 'packages/core/src/core/client.test.ts')).toHaveLength(0);
+ expect(runRule("import value from '../../index.js';", 'packages/core/src/fixtures/client.ts')).toHaveLength(0);
+ expect(runRule("import value from '@qwen-code/qwen-code-core';", 'packages/cli/src/index.ts')).toHaveLength(0);
+ });
+
+ it('allows direct owner imports', () => {
+ expect(runRule("import value from '../tools/tools.js';", 'packages/core/src/core/client.ts')).toHaveLength(0);
+ });
+});