Files
qwen-code/eslint-rules/no-config-object-create.js
T
易良andShaojin Wen 6652fdc9f6 refactor(core): centralize worktree config derivation (#9915)
* refactor(core): centralize worktree config derivation

* refactor(core): centralize agent config derivation (#9918)

* refactor(core): centralize agent config derivation

* refactor(core): centralize scoped config profiles (#9920)

* fix(core): keep approval-mode transitions working on derived teammate and dispatch configs

Give every per-agent config child-local approval state, not just the ones
spawned with an explicit mode: tools bind to the config and teammate mode
switches (Shift+Tab) plus 'Proceed always' confirmations call setApprovalMode
on it, which the derived-Config guard rejects without an approval profile.
Snapshot the base mode when none is requested so no AUTO strip is acquired
and cleanup stays a no-op.

Layer an approval profile over the workflow-orchestrator's derived dispatch
contexts (worktree isolation / workingDir) as agent.ts does, with cleanup in
the outer finally.

* fix(core): anchor plans-directory assertions at the plans-owning base Config

Derived agent profiles rebind targetDir to their own workspace, but the plan
file stays in the owning base's configured plans directory. The containment
assertions in savePlan/loadPlan compared against the derived workspace and
threw FatalConfigError for teammates whose cwd differs from the parent project
root, which savePlanBestEffort swallowed into a debug warning — silently
dropping the plan. Resolve the anchor to the Config that owns the
plans-directory state instead.

* test(core): cover deriveAgentConfig workspace rebinds

Mirror the deriveWorktreeConfig pairing test: assert the public getter
overrides (getCwd/getProjectRoot/getWorkingDir/getTargetDir, plan-file path)
and the paired private targetDir/cwd field writes plus own
workspaceContext/fileDiscoveryService, so a refactor that drops any rebind
fails loudly instead of leaking parent-tree reads into spawned agents.

* test(scripts): add flat-config integration test for no-config-object-create

Mirror no-core-root-barrel-config.test.js: load the real eslint.config.js via
new ESLint({overrideConfigFile}) and lintText virtual packages/core/src/**
paths, so a future edit to the flat-config wiring (plugin key, rule name,
ignores) that silently stops the gate applying to packages/core/src/** turns
this test red instead of degrading the invariant with no signal.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-08-27 12:28:08 +00:00

60 lines
1.6 KiB
JavaScript

function importsConfig(node) {
return (
typeof node.source.value === 'string' &&
node.source.value.endsWith('/config/config.js') &&
node.specifiers.some(
(specifier) =>
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name === 'Config',
)
);
}
function isObjectCreate(node) {
return (
node.callee.type === 'MemberExpression' &&
!node.callee.computed &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Object' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'create' &&
!(node.arguments[0]?.type === 'Literal' && node.arguments[0].value === null)
);
}
export default {
meta: {
type: 'problem',
docs: {
description:
'Require Config prototype derivation to go through deriveConfig.',
},
schema: [],
messages: {
useDeriveConfig:
'Do not derive Config with Object.create(). Use deriveConfig() or a specialized Config factory.',
},
},
create(context) {
let hasConfigImport = false;
const objectCreateCalls = [];
return {
ImportDeclaration(node) {
if (importsConfig(node)) hasConfigImport = true;
},
CallExpression(node) {
if (isObjectCreate(node)) objectCreateCalls.push(node);
},
'Program:exit'() {
if (!hasConfigImport) return;
for (const node of objectCreateCalls) {
context.report({ node, messageId: 'useDeriveConfig' });
}
},
};
},
};