chore(lint): restore zero-error eslint baseline

This commit is contained in:
William Valentin
2026-02-15 22:25:29 -08:00
parent 8b529a18f2
commit 46538e71a8
11 changed files with 184 additions and 160 deletions
@@ -20,6 +20,7 @@ Scope: Production-risk-first audit of bugs, code improvements, and feature oppor
- ✅ F-015 addressed: retry defaults no longer classify timeout-style failures as non-retryable, improving resilience for transient timeout conditions. - ✅ F-015 addressed: retry defaults no longer classify timeout-style failures as non-retryable, improving resilience for transient timeout conditions.
- ✅ F-011 addressed: Slack user-name resolution now uses bounded TTL+LRU caching to prevent unbounded growth. - ✅ F-011 addressed: Slack user-name resolution now uses bounded TTL+LRU caching to prevent unbounded growth.
- ◑ F-013 partially addressed: reset-command normalization is now shared across Discord/Slack/WhatsApp adapters via `src/channels/utils.ts`, reducing duplicated command-parsing logic. - ◑ F-013 partially addressed: reset-command normalization is now shared across Discord/Slack/WhatsApp adapters via `src/channels/utils.ts`, reducing duplicated command-parsing logic.
- ◑ F-004 partially addressed: lint error baseline is restored (`pnpm lint` now passes with 0 errors), while warning-burn-down remains open.
## Executive Summary ## Executive Summary
@@ -27,14 +28,14 @@ Current health snapshot:
- `pnpm typecheck`: passing - `pnpm typecheck`: passing
- `pnpm build`: passing - `pnpm build`: passing
- `pnpm test:run`: passing (`140/140` files, `1773/1773` tests) - `pnpm test:run`: passing (`140/140` files, `1773/1773` tests)
- `pnpm lint`: failing (`148 errors`, `530 warnings`) - `pnpm lint`: passing with warnings only (`0 errors`, `539 warnings`)
Top conclusions: Top conclusions:
- A critical Web UI security issue exists in markdown rendering (unsanitized HTML insertion). - A critical Web UI security issue exists in markdown rendering (unsanitized HTML insertion).
- Runtime configuration edits from the settings page appear non-persistent across restart. - Runtime configuration edits from the settings page appear non-persistent across restart.
- Tool timeout behavior likely allows underlying side effects to continue after timeout. - Tool timeout behavior likely allows underlying side effects to continue after timeout.
- Gateway request-body handling and WebSocket ingress controls need abuse protections. - Gateway request-body handling and WebSocket ingress controls need abuse protections.
- Lint quality gates are currently broken at scale, reducing CI signal quality. - Lint error-level gate is restored, but warning debt remains high.
## Methodology and Scope ## Methodology and Scope
@@ -125,7 +126,7 @@ Remediation update (2026-02-16):
- Severity: Medium - Severity: Medium
- Impact: CI noise, reduced confidence in static analysis, and slower defect detection. - Impact: CI noise, reduced confidence in static analysis, and slower defect detection.
- Evidence: - Evidence:
- `pnpm -s lint` => `148 errors`, `530 warnings` - `pnpm -s lint` => `0 errors`, `539 warnings`
- Error concentration: - Error concentration:
- `src/daemon/models.ts` (90 errors) - `src/daemon/models.ts` (90 errors)
- `src/cli/tui.ts` (25 errors) - `src/cli/tui.ts` (25 errors)
@@ -142,6 +143,10 @@ Remediation update (2026-02-16):
- CI check enforcing `eslint` errors = 0. - CI check enforcing `eslint` errors = 0.
- Secondary threshold check for warning reduction trend. - Secondary threshold check for warning reduction trend.
Remediation update (2026-02-16):
- Stage 1 complete: fixed all error-level ESLint violations in impacted high-error files so `pnpm lint` now passes with `0` errors.
- Stage 2 pending: warning-burn-down remains (currently `539` warnings).
### F-005 Medium: ESLint browser globals mismatch causes avoidable UI lint failures ### F-005 Medium: ESLint browser globals mismatch causes avoidable UI lint failures
- Severity: Medium - Severity: Medium
@@ -443,9 +448,9 @@ pnpm -s lint
Observed outcomes: Observed outcomes:
- Typecheck/build/test: passing. - Typecheck/build/test: passing.
- Lint: failing with `148 errors` and `530 warnings`. - Lint: passing with warnings only (`0` errors, `539` warnings).
Top lint error concentration snapshot: Historical pre-remediation lint error concentration snapshot:
- `src/daemon/models.ts`: 90 errors - `src/daemon/models.ts`: 90 errors
- `src/cli/tui.ts`: 25 errors - `src/cli/tui.ts`: 25 errors
- `src/daemon/routing.ts`: 14 errors - `src/daemon/routing.ts`: 14 errors
+19
View File
@@ -2628,6 +2628,25 @@
"docs/plans/analysis/2026-02-16-codebase-audit-report.md" "docs/plans/analysis/2026-02-16-codebase-audit-report.md"
], ],
"test_status": "pnpm test:run src/channels/utils.test.ts src/channels/discord/adapter.test.ts src/channels/slack/adapter.test.ts src/channels/whatsapp/adapter.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/channels/utils.test.ts src/channels/discord/adapter.test.ts src/channels/slack/adapter.test.ts src/channels/whatsapp/adapter.test.ts + pnpm typecheck passing"
},
"audit-followup-lint-error-baseline": {
"status": "completed",
"date": "2026-02-16",
"updated": "2026-02-16",
"summary": "Completed stage-1 lint recovery by clearing all error-level ESLint violations in high-error files (`daemon/models.ts`, `cli/tui.ts`, `daemon/routing.ts`, `gateway/ui/pages/settings.js`) and adjacent return-await/no-useless-return issues so `pnpm lint` now passes with warnings only.",
"files_modified": [
"src/daemon/models.ts",
"src/cli/tui.ts",
"src/daemon/routing.ts",
"src/gateway/ui/pages/settings.js",
"src/backends/native/orchestrator.ts",
"src/frontends/tui/components/App.tsx",
"src/gateway/server.test.ts",
"src/hooks/engine.ts",
"src/tools/executor.test.ts",
"docs/plans/analysis/2026-02-16-codebase-audit-report.md"
],
"test_status": "pnpm test:run src/gateway/server.test.ts src/tools/executor.test.ts src/backends/native/orchestrator.test.ts src/daemon/routing.test.ts + pnpm typecheck + pnpm lint passing (0 errors, warnings remain)"
} }
}, },
"overall_progress": { "overall_progress": {
+1 -1
View File
@@ -504,7 +504,7 @@ export class AgentOrchestrator {
private _restoreHistory(messages: Message[]): void { private _restoreHistory(messages: Message[]): void {
if (this._session) { if (this._session) {
this._session.replaceHistory(messages); this._session.replaceHistory(messages);
return;
} }
// No session available; nothing safe to do here. // No session available; nothing safe to do here.
} }
+25 -25
View File
@@ -232,33 +232,33 @@ export function registerTuiCommand(program: Command): void {
process.exit(0); process.exit(0);
}); });
if (opts.fullscreen) { if (opts.fullscreen) {
await startFullscreenTui({ await startFullscreenTui({
session, session,
modelClient: modelRouter, modelClient: modelRouter,
modelRouter, modelRouter,
systemPrompt, systemPrompt,
model: config.models.default.model, model: config.models.default.model,
agent, agent,
hookEngine, hookEngine,
modelProviderConfigs, modelProviderConfigs,
onExit: cleanup, onExit: cleanup,
}); });
} else { } else {
let switchingToFullscreen = false; let switchingToFullscreen = false;
const tui = new MinimalTui({ const tui = new MinimalTui({
session, session,
modelClient: modelRouter, modelClient: modelRouter,
modelRouter, modelRouter,
systemPrompt, systemPrompt,
agent, agent,
hookEngine, hookEngine,
pairingManager, pairingManager,
localProviders: config.models.local_providers, localProviders: config.models.local_providers,
modelProviderConfigs, modelProviderConfigs,
currentLocalProvider: config.models.local?.provider, currentLocalProvider: config.models.local?.provider,
onTransfer: (target) => { onTransfer: (target) => {
if (target === 'telegram') { if (target === 'telegram') {
if (config.telegram && config.telegram.allowed_chat_ids.length > 0) { if (config.telegram && config.telegram.allowed_chat_ids.length > 0) {
const telegramUserId = String(config.telegram.allowed_chat_ids[0]); const telegramUserId = String(config.telegram.allowed_chat_ids[0]);
+96 -96
View File
@@ -57,115 +57,115 @@ function resolveZaiCredential(cfg: ModelConfig): string {
export function createClientFromConfig(cfg: ModelConfig): ModelClient { export function createClientFromConfig(cfg: ModelConfig): ModelClient {
switch (cfg.provider) { switch (cfg.provider) {
case 'anthropic': case 'anthropic':
{ {
const authMode = getEffectiveAuthMode(cfg); const authMode = getEffectiveAuthMode(cfg);
if (authMode === 'oauth') {
const token = cfg.auth_token ?? getAnthropicAuthToken();
if (!token) {
throw new Error(
'Anthropic auth token not configured (auth_mode: oauth). ' +
'Set ANTHROPIC_AUTH_TOKEN, run `flynn anthropic-auth --token`, or provide auth_token in config.',
);
}
return new AnthropicClient({
model: cfg.model,
authToken: token,
});
}
if (authMode === 'api_key') {
const apiKey = cfg.api_key ?? getAnthropicApiKey();
if (!apiKey) {
throw new Error(
'Anthropic API key not configured (auth_mode: api_key). ' +
'Set ANTHROPIC_API_KEY, run `flynn anthropic-auth`, or provide api_key in config.',
);
}
return new AnthropicClient({
model: cfg.model,
apiKey,
});
}
// auto: prefer API key, then token
const apiKey = cfg.api_key ?? getAnthropicApiKey();
if (apiKey) {
return new AnthropicClient({
model: cfg.model,
apiKey,
});
}
if (authMode === 'oauth') {
const token = cfg.auth_token ?? getAnthropicAuthToken(); const token = cfg.auth_token ?? getAnthropicAuthToken();
if (token) { if (!token) {
return new AnthropicClient({ throw new Error(
model: cfg.model, 'Anthropic auth token not configured (auth_mode: oauth). ' +
authToken: token, 'Set ANTHROPIC_AUTH_TOKEN, run `flynn anthropic-auth --token`, or provide auth_token in config.',
}); );
} }
return new AnthropicClient({
model: cfg.model,
authToken: token,
});
}
throw new Error( if (authMode === 'api_key') {
'Anthropic credentials not configured (auth_mode: auto). ' + const apiKey = cfg.api_key ?? getAnthropicApiKey();
if (!apiKey) {
throw new Error(
'Anthropic API key not configured (auth_mode: api_key). ' +
'Set ANTHROPIC_API_KEY, run `flynn anthropic-auth`, or provide api_key in config.',
);
}
return new AnthropicClient({
model: cfg.model,
apiKey,
});
}
// auto: prefer API key, then token
const apiKey = cfg.api_key ?? getAnthropicApiKey();
if (apiKey) {
return new AnthropicClient({
model: cfg.model,
apiKey,
});
}
const token = cfg.auth_token ?? getAnthropicAuthToken();
if (token) {
return new AnthropicClient({
model: cfg.model,
authToken: token,
});
}
throw new Error(
'Anthropic credentials not configured (auth_mode: auto). ' +
'Set ANTHROPIC_API_KEY (or run `flynn anthropic-auth`), ' + 'Set ANTHROPIC_API_KEY (or run `flynn anthropic-auth`), ' +
'or set ANTHROPIC_AUTH_TOKEN (or run `flynn anthropic-auth --token`).', 'or set ANTHROPIC_AUTH_TOKEN (or run `flynn anthropic-auth --token`).',
); );
} }
case 'openai': case 'openai':
{ {
const authMode = getEffectiveAuthMode(cfg); const authMode = getEffectiveAuthMode(cfg);
if (authMode === 'oauth') {
const existing = loadStoredOpenAIAuth();
if (!existing) {
throw new Error(
'OpenAI OAuth is not configured (auth_mode: oauth). ' +
'Run `flynn openai-auth` to authenticate.',
);
}
return new OpenAIClient({
model: cfg.model,
useOAuth: true,
});
}
if (authMode === 'api_key') {
const apiKey = cfg.api_key ?? getOpenAIApiKey();
if (!apiKey) {
throw new Error(
'OpenAI API key not configured (auth_mode: api_key). ' +
'Set OPENAI_API_KEY, run `flynn openai-key`, or provide api_key in config.',
);
}
return new OpenAIClient({
model: cfg.model,
apiKey,
});
}
// auto: prefer API key, then OAuth
const apiKey = cfg.api_key ?? getOpenAIApiKey();
if (apiKey) {
return new OpenAIClient({
model: cfg.model,
apiKey,
});
}
if (authMode === 'oauth') {
const existing = loadStoredOpenAIAuth(); const existing = loadStoredOpenAIAuth();
if (existing) { if (!existing) {
return new OpenAIClient({ throw new Error(
model: cfg.model, 'OpenAI OAuth is not configured (auth_mode: oauth). ' +
useOAuth: true, 'Run `flynn openai-auth` to authenticate.',
}); );
} }
return new OpenAIClient({
model: cfg.model,
useOAuth: true,
});
}
throw new Error( if (authMode === 'api_key') {
'OpenAI credentials not configured (auth_mode: auto). ' + const apiKey = cfg.api_key ?? getOpenAIApiKey();
if (!apiKey) {
throw new Error(
'OpenAI API key not configured (auth_mode: api_key). ' +
'Set OPENAI_API_KEY, run `flynn openai-key`, or provide api_key in config.',
);
}
return new OpenAIClient({
model: cfg.model,
apiKey,
});
}
// auto: prefer API key, then OAuth
const apiKey = cfg.api_key ?? getOpenAIApiKey();
if (apiKey) {
return new OpenAIClient({
model: cfg.model,
apiKey,
});
}
const existing = loadStoredOpenAIAuth();
if (existing) {
return new OpenAIClient({
model: cfg.model,
useOAuth: true,
});
}
throw new Error(
'OpenAI credentials not configured (auth_mode: auto). ' +
'Set OPENAI_API_KEY (or run `flynn openai-key`), ' + 'Set OPENAI_API_KEY (or run `flynn openai-key`), ' +
'or run `flynn openai-auth` for OAuth.', 'or run `flynn openai-auth` for OAuth.',
); );
} }
case 'ollama': case 'ollama':
return new OllamaClient({ return new OllamaClient({
model: cfg.model, model: cfg.model,
+14 -14
View File
@@ -199,7 +199,7 @@ export function createMessageRouter(deps: {
effectiveToolRegistry = effectiveToolRegistry.clone(); effectiveToolRegistry = effectiveToolRegistry.clone();
effectiveToolRegistry.register(createMediaSendTool(collector)); effectiveToolRegistry.register(createMediaSendTool(collector));
const orchestrator = new AgentOrchestrator({ const orchestrator = new AgentOrchestrator({
modelRouter: deps.modelRouter, modelRouter: deps.modelRouter,
systemPrompt: effectiveSystemPrompt, systemPrompt: effectiveSystemPrompt,
session, session,
@@ -221,19 +221,19 @@ export function createMessageRouter(deps: {
memoryAutoExtract: deps.config.memory?.auto_extract, memoryAutoExtract: deps.config.memory?.auto_extract,
memoryInjectionStrategy: deps.config.memory?.injection_strategy, memoryInjectionStrategy: deps.config.memory?.injection_strategy,
memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens, memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens,
toolPolicyContext: { toolPolicyContext: {
agent: effectiveTier, agent: effectiveTier,
provider: effectiveProvider, provider: effectiveProvider,
sessionId: session.id, sessionId: session.id,
channel, channel,
sender: senderId, sender: senderId,
tier: effectiveTier, tier: effectiveTier,
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard', autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
skillName: activeSkillName, skillName: activeSkillName,
skillPermissions: activeSkill?.manifest.permissions, skillPermissions: activeSkill?.manifest.permissions,
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets, allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
executionEnvironment, executionEnvironment,
}, },
attachmentCollector: collector, attachmentCollector: collector,
}); });
entry = { orchestrator, collector }; entry = { orchestrator, collector };
+1 -1
View File
@@ -117,7 +117,7 @@ export function App({
if (!hookEngine) {return;} if (!hookEngine) {return;}
hookEngine.setInteractiveConfirmer(async (pending) => { hookEngine.setInteractiveConfirmer(async (pending) => {
return await new Promise<HookResult>((resolve) => { return new Promise<HookResult>((resolve) => {
confirmResolveRef.current = resolve; confirmResolveRef.current = resolve;
setConfirmation({ tool: pending.tool, args: pending.args }); setConfirmation({ tool: pending.tool, args: pending.args });
}); });
+1 -1
View File
@@ -8,7 +8,7 @@ import type { GatewayResponse, GatewayError, GatewayEvent } from './protocol.js'
import { ErrorCode } from './protocol.js'; import { ErrorCode } from './protocol.js';
async function canListenOnLocalhost(): Promise<boolean> { async function canListenOnLocalhost(): Promise<boolean> {
return await new Promise((resolvePromise) => { return new Promise((resolvePromise) => {
const s = createServer(); const s = createServer();
s.once('error', () => resolvePromise(false)); s.once('error', () => resolvePromise(false));
s.listen(0, '127.0.0.1', () => { s.listen(0, '127.0.0.1', () => {
+14 -14
View File
@@ -17,7 +17,7 @@ let _el = null;
async function loadSettings() { async function loadSettings() {
if (!_client || !_el) {return;} if (!_client || !_el) {return;}
let config, tools, channels; let config, tools, channels;
let services; let services;
try { try {
@@ -101,18 +101,18 @@ async function loadSettings() {
${serviceList.length > 0 ? ` ${serviceList.length > 0 ? `
<div class="services-grid"> <div class="services-grid">
${serviceList.map(svc => { ${serviceList.map(svc => {
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧'; const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
const statusClass = svc.status === 'connected' const statusClass = svc.status === 'connected'
? 'connected' ? 'connected'
: svc.status === 'configured' : svc.status === 'configured'
? 'configured' ? 'configured'
: svc.status === 'error' : svc.status === 'error'
? 'error' ? 'error'
: svc.status === 'not_configured' : svc.status === 'not_configured'
? 'not-configured' ? 'not-configured'
: 'disconnected'; : 'disconnected';
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : ''; const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
return ` return `
<div class="service-card service-${statusClass}"> <div class="service-card service-${statusClass}">
<span class="service-type-icon">${typeIcon}</span> <span class="service-type-icon">${typeIcon}</span>
<span class="service-name">${escapeHtml(svc.name)}${itemCount}</span> <span class="service-name">${escapeHtml(svc.name)}${itemCount}</span>
@@ -120,7 +120,7 @@ async function loadSettings() {
<span class="service-description text-muted text-xs">${escapeHtml(svc.description ?? '')}</span> <span class="service-description text-muted text-xs">${escapeHtml(svc.description ?? '')}</span>
</div> </div>
`; `;
}).join('')} }).join('')}
</div> </div>
` : '<div class="text-muted text-sm">No services found</div>'} ` : '<div class="text-muted text-sm">No services found</div>'}
</div> </div>
+1 -1
View File
@@ -48,7 +48,7 @@ export class HookEngine {
const id = randomUUID(); const id = randomUUID();
if (this.interactiveConfirmer) { if (this.interactiveConfirmer) {
return await this.interactiveConfirmer({ id, tool, args }); return this.interactiveConfirmer({ id, tool, args });
} }
return new Promise((resolve) => { return new Promise((resolve) => {
+2 -2
View File
@@ -47,7 +47,7 @@ const cancellableTool: Tool = {
description: 'Long-running cancellable tool', description: 'Long-running cancellable tool',
inputSchema: { type: 'object', properties: {} }, inputSchema: { type: 'object', properties: {} },
execute: async (_args, context) => { execute: async (_args, context) => {
return await new Promise((resolve) => { return new Promise((resolve) => {
const onAbort = () => resolve({ success: false, output: '', error: 'aborted' }); const onAbort = () => resolve({ success: false, output: '', error: 'aborted' });
if (context?.signal?.aborted) { if (context?.signal?.aborted) {
onAbort(); onAbort();
@@ -65,7 +65,7 @@ function createSideEffectTool(sideEffect: { fired: boolean }): Tool {
description: 'Cancellable side effect', description: 'Cancellable side effect',
inputSchema: { type: 'object', properties: {} }, inputSchema: { type: 'object', properties: {} },
execute: async (_args, context) => { execute: async (_args, context) => {
return await new Promise((resolve) => { return new Promise((resolve) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
sideEffect.fired = true; sideEffect.fired = true;
resolve({ success: true, output: 'side effect fired' }); resolve({ success: true, output: 'side effect fired' });