diff --git a/docs/plans/analysis/2026-02-16-codebase-audit-report.md b/docs/plans/analysis/2026-02-16-codebase-audit-report.md index eb675de..0f1abcc 100644 --- a/docs/plans/analysis/2026-02-16-codebase-audit-report.md +++ b/docs/plans/analysis/2026-02-16-codebase-audit-report.md @@ -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-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-004 partially addressed: lint error baseline is restored (`pnpm lint` now passes with 0 errors), while warning-burn-down remains open. ## Executive Summary @@ -27,14 +28,14 @@ Current health snapshot: - `pnpm typecheck`: passing - `pnpm build`: passing - `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: - 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. - Tool timeout behavior likely allows underlying side effects to continue after timeout. - 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 @@ -125,7 +126,7 @@ Remediation update (2026-02-16): - Severity: Medium - Impact: CI noise, reduced confidence in static analysis, and slower defect detection. - Evidence: - - `pnpm -s lint` => `148 errors`, `530 warnings` + - `pnpm -s lint` => `0 errors`, `539 warnings` - Error concentration: - `src/daemon/models.ts` (90 errors) - `src/cli/tui.ts` (25 errors) @@ -142,6 +143,10 @@ Remediation update (2026-02-16): - CI check enforcing `eslint` errors = 0. - 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 - Severity: Medium @@ -443,9 +448,9 @@ pnpm -s lint Observed outcomes: - 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/cli/tui.ts`: 25 errors - `src/daemon/routing.ts`: 14 errors diff --git a/docs/plans/state.json b/docs/plans/state.json index eb549fd..f4e0c81 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2628,6 +2628,25 @@ "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" + }, + "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": { diff --git a/src/backends/native/orchestrator.ts b/src/backends/native/orchestrator.ts index 6c59c83..99fc103 100644 --- a/src/backends/native/orchestrator.ts +++ b/src/backends/native/orchestrator.ts @@ -504,7 +504,7 @@ export class AgentOrchestrator { private _restoreHistory(messages: Message[]): void { if (this._session) { this._session.replaceHistory(messages); - return; + } // No session available; nothing safe to do here. } diff --git a/src/cli/tui.ts b/src/cli/tui.ts index 203690b..2bdf91f 100644 --- a/src/cli/tui.ts +++ b/src/cli/tui.ts @@ -232,33 +232,33 @@ export function registerTuiCommand(program: Command): void { process.exit(0); }); - if (opts.fullscreen) { - await startFullscreenTui({ - session, - modelClient: modelRouter, - modelRouter, - systemPrompt, - model: config.models.default.model, - agent, - hookEngine, - modelProviderConfigs, - onExit: cleanup, - }); - } else { + if (opts.fullscreen) { + await startFullscreenTui({ + session, + modelClient: modelRouter, + modelRouter, + systemPrompt, + model: config.models.default.model, + agent, + hookEngine, + modelProviderConfigs, + onExit: cleanup, + }); + } else { let switchingToFullscreen = false; - const tui = new MinimalTui({ - session, - modelClient: modelRouter, - modelRouter, - systemPrompt, - agent, - hookEngine, - pairingManager, - localProviders: config.models.local_providers, - modelProviderConfigs, - currentLocalProvider: config.models.local?.provider, - onTransfer: (target) => { + const tui = new MinimalTui({ + session, + modelClient: modelRouter, + modelRouter, + systemPrompt, + agent, + hookEngine, + pairingManager, + localProviders: config.models.local_providers, + modelProviderConfigs, + currentLocalProvider: config.models.local?.provider, + onTransfer: (target) => { if (target === 'telegram') { if (config.telegram && config.telegram.allowed_chat_ids.length > 0) { const telegramUserId = String(config.telegram.allowed_chat_ids[0]); diff --git a/src/daemon/models.ts b/src/daemon/models.ts index ac9dd66..d6061ea 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -57,115 +57,115 @@ function resolveZaiCredential(cfg: ModelConfig): string { export function createClientFromConfig(cfg: ModelConfig): ModelClient { switch (cfg.provider) { case 'anthropic': - { - 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, - }); - } + { + const authMode = getEffectiveAuthMode(cfg); + if (authMode === 'oauth') { const token = cfg.auth_token ?? getAnthropicAuthToken(); - if (token) { - return new AnthropicClient({ - model: cfg.model, - authToken: token, - }); + 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, + }); + } - throw new Error( - 'Anthropic credentials not configured (auth_mode: auto). ' + + 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, + }); + } + + 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`), ' + 'or set ANTHROPIC_AUTH_TOKEN (or run `flynn anthropic-auth --token`).', - ); - } + ); + } case 'openai': - { - 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, - }); - } + { + const authMode = getEffectiveAuthMode(cfg); + if (authMode === 'oauth') { const existing = loadStoredOpenAIAuth(); - if (existing) { - return new OpenAIClient({ - model: cfg.model, - useOAuth: true, - }); + 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, + }); + } - throw new Error( - 'OpenAI credentials not configured (auth_mode: auto). ' + + 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, + }); + } + + 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`), ' + 'or run `flynn openai-auth` for OAuth.', - ); - } + ); + } case 'ollama': return new OllamaClient({ model: cfg.model, diff --git a/src/daemon/routing.ts b/src/daemon/routing.ts index 1f604d5..5e9f86c 100644 --- a/src/daemon/routing.ts +++ b/src/daemon/routing.ts @@ -199,7 +199,7 @@ export function createMessageRouter(deps: { effectiveToolRegistry = effectiveToolRegistry.clone(); effectiveToolRegistry.register(createMediaSendTool(collector)); - const orchestrator = new AgentOrchestrator({ + const orchestrator = new AgentOrchestrator({ modelRouter: deps.modelRouter, systemPrompt: effectiveSystemPrompt, session, @@ -221,19 +221,19 @@ export function createMessageRouter(deps: { memoryAutoExtract: deps.config.memory?.auto_extract, memoryInjectionStrategy: deps.config.memory?.injection_strategy, memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens, - toolPolicyContext: { - agent: effectiveTier, - provider: effectiveProvider, - sessionId: session.id, - channel, - sender: senderId, - tier: effectiveTier, - autonomyLevel: deps.config.agents.autonomy_level ?? 'standard', - skillName: activeSkillName, - skillPermissions: activeSkill?.manifest.permissions, - allowedSecretScopes: activeSkill?.manifest.permissions?.secrets, - executionEnvironment, - }, + toolPolicyContext: { + agent: effectiveTier, + provider: effectiveProvider, + sessionId: session.id, + channel, + sender: senderId, + tier: effectiveTier, + autonomyLevel: deps.config.agents.autonomy_level ?? 'standard', + skillName: activeSkillName, + skillPermissions: activeSkill?.manifest.permissions, + allowedSecretScopes: activeSkill?.manifest.permissions?.secrets, + executionEnvironment, + }, attachmentCollector: collector, }); entry = { orchestrator, collector }; diff --git a/src/frontends/tui/components/App.tsx b/src/frontends/tui/components/App.tsx index 7373bf8..10bb81c 100644 --- a/src/frontends/tui/components/App.tsx +++ b/src/frontends/tui/components/App.tsx @@ -117,7 +117,7 @@ export function App({ if (!hookEngine) {return;} hookEngine.setInteractiveConfirmer(async (pending) => { - return await new Promise((resolve) => { + return new Promise((resolve) => { confirmResolveRef.current = resolve; setConfirmation({ tool: pending.tool, args: pending.args }); }); diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index e2fe7c7..3ba146a 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -8,7 +8,7 @@ import type { GatewayResponse, GatewayError, GatewayEvent } from './protocol.js' import { ErrorCode } from './protocol.js'; async function canListenOnLocalhost(): Promise { - return await new Promise((resolvePromise) => { + return new Promise((resolvePromise) => { const s = createServer(); s.once('error', () => resolvePromise(false)); s.listen(0, '127.0.0.1', () => { diff --git a/src/gateway/ui/pages/settings.js b/src/gateway/ui/pages/settings.js index ca27f57..01eb2e4 100644 --- a/src/gateway/ui/pages/settings.js +++ b/src/gateway/ui/pages/settings.js @@ -17,7 +17,7 @@ let _el = null; async function loadSettings() { if (!_client || !_el) {return;} - let config, tools, channels; + let config, tools, channels; let services; try { @@ -101,18 +101,18 @@ async function loadSettings() { ${serviceList.length > 0 ? `
${serviceList.map(svc => { - const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧'; - const statusClass = svc.status === 'connected' - ? 'connected' - : svc.status === 'configured' - ? 'configured' - : svc.status === 'error' - ? 'error' - : svc.status === 'not_configured' - ? 'not-configured' - : 'disconnected'; - const itemCount = svc.itemCount ? ` (${svc.itemCount})` : ''; - return ` + const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧'; + const statusClass = svc.status === 'connected' + ? 'connected' + : svc.status === 'configured' + ? 'configured' + : svc.status === 'error' + ? 'error' + : svc.status === 'not_configured' + ? 'not-configured' + : 'disconnected'; + const itemCount = svc.itemCount ? ` (${svc.itemCount})` : ''; + return `
${typeIcon} ${escapeHtml(svc.name)}${itemCount} @@ -120,7 +120,7 @@ async function loadSettings() { ${escapeHtml(svc.description ?? '')}
`; - }).join('')} + }).join('')}
` : '
No services found
'} diff --git a/src/hooks/engine.ts b/src/hooks/engine.ts index 13b2501..197ed68 100644 --- a/src/hooks/engine.ts +++ b/src/hooks/engine.ts @@ -48,7 +48,7 @@ export class HookEngine { const id = randomUUID(); if (this.interactiveConfirmer) { - return await this.interactiveConfirmer({ id, tool, args }); + return this.interactiveConfirmer({ id, tool, args }); } return new Promise((resolve) => { diff --git a/src/tools/executor.test.ts b/src/tools/executor.test.ts index 28be135..7cb86f9 100644 --- a/src/tools/executor.test.ts +++ b/src/tools/executor.test.ts @@ -47,7 +47,7 @@ const cancellableTool: Tool = { description: 'Long-running cancellable tool', inputSchema: { type: 'object', properties: {} }, execute: async (_args, context) => { - return await new Promise((resolve) => { + return new Promise((resolve) => { const onAbort = () => resolve({ success: false, output: '', error: 'aborted' }); if (context?.signal?.aborted) { onAbort(); @@ -65,7 +65,7 @@ function createSideEffectTool(sideEffect: { fired: boolean }): Tool { description: 'Cancellable side effect', inputSchema: { type: 'object', properties: {} }, execute: async (_args, context) => { - return await new Promise((resolve) => { + return new Promise((resolve) => { const timer = setTimeout(() => { sideEffect.fired = true; resolve({ success: true, output: 'side effect fired' });