Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ import { createCodexRoutes } from './routes/codex/index.js';
import { CodexUsageService } from './services/codex-usage-service.js';
import { CodexAppServerService } from './services/codex-app-server-service.js';
import { CodexModelCacheService } from './services/codex-model-cache-service.js';
import { createZaiRoutes } from './routes/zai/index.js';
import { ZaiUsageService } from './services/zai-usage-service.js';
import { createGeminiRoutes } from './routes/gemini/index.js';
import { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
Expand Down Expand Up @@ -300,7 +303,7 @@ app.use(
callback(null, origin);
return;
}
} catch (err) {
} catch {
// Ignore URL parsing errors
}

Expand Down Expand Up @@ -328,6 +331,7 @@ const claudeUsageService = new ClaudeUsageService();
const codexAppServerService = new CodexAppServerService();
const codexModelCacheService = new CodexModelCacheService(DATA_DIR, codexAppServerService);
const codexUsageService = new CodexUsageService(codexAppServerService);
const zaiUsageService = new ZaiUsageService();
const mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader);

Expand Down Expand Up @@ -372,7 +376,7 @@ eventHookService.initialize(events, settingsService, eventHistoryService, featur
let globalSettings: Awaited<ReturnType<typeof settingsService.getGlobalSettings>> | null = null;
try {
globalSettings = await settingsService.getGlobalSettings();
} catch (err) {
} catch {
logger.warn('Failed to load global settings, using defaults');
}

Expand All @@ -390,7 +394,7 @@ eventHookService.initialize(events, settingsService, eventHistoryService, featur
const enableRequestLog = globalSettings.enableRequestLogging ?? true;
setRequestLoggingEnabled(enableRequestLog);
logger.info(`HTTP request logging: ${enableRequestLog ? 'enabled' : 'disabled'}`);
} catch (err) {
} catch {
logger.warn('Failed to apply logging settings, using defaults');
}
}
Expand All @@ -417,6 +421,22 @@ eventHookService.initialize(events, settingsService, eventHistoryService, featur
} else {
logger.info('[STARTUP] Feature state reconciliation complete - no stale states found');
}

// Resume interrupted features in the background after reconciliation.
// This uses the saved execution state to identify features that were running
// before the restart (their statuses have been reset to ready/backlog by
// reconciliation above). Running in background so it doesn't block startup.
if (totalReconciled > 0) {
for (const project of globalSettings.projects) {
autoModeService.resumeInterruptedFeatures(project.path).catch((err) => {
logger.warn(
`[STARTUP] Failed to resume interrupted features for ${project.path}:`,
err
);
});
}
logger.info('[STARTUP] Initiated background resume of interrupted features');
}
}
} catch (err) {
logger.warn('[STARTUP] Failed to reconcile feature states:', err);
Expand Down Expand Up @@ -473,6 +493,8 @@ app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/codex', createCodexRoutes(codexUsageService, codexModelCacheService));
app.use('/api/zai', createZaiRoutes(zaiUsageService, settingsService));
app.use('/api/gemini', createGeminiRoutes());
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));
Expand Down Expand Up @@ -575,7 +597,7 @@ wss.on('connection', (ws: WebSocket) => {
logger.info('Sending event to client:', {
type,
messageLength: message.length,
sessionId: (payload as any)?.sessionId,
sessionId: (payload as Record<string, unknown>)?.sessionId,
});
ws.send(message);
} else {
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/lib/cli-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import { spawn, execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { createLogger } from '@automaker/utils';

const logger = createLogger('CliDetection');

export interface CliInfo {
name: string;
Expand Down Expand Up @@ -86,7 +83,7 @@ export async function detectCli(
options: CliDetectionOptions = {}
): Promise<CliDetectionResult> {
const config = CLI_CONFIGS[provider];
const { timeout = 5000, includeWsl = false, wslDistribution } = options;
const { timeout = 5000 } = options;
const issues: string[] = [];

const cliInfo: CliInfo = {
Expand Down
21 changes: 11 additions & 10 deletions apps/server/src/lib/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface ErrorClassification {
suggestedAction?: string;
retryable: boolean;
provider?: string;
context?: Record<string, any>;
context?: Record<string, unknown>;
}

export interface ErrorPattern {
Expand Down Expand Up @@ -180,7 +180,7 @@ const ERROR_PATTERNS: ErrorPattern[] = [
export function classifyError(
error: unknown,
provider?: string,
context?: Record<string, any>
context?: Record<string, unknown>
): ErrorClassification {
const errorText = getErrorText(error);

Expand Down Expand Up @@ -281,18 +281,19 @@ function getErrorText(error: unknown): string {

if (typeof error === 'object' && error !== null) {
// Handle structured error objects
const errorObj = error as any;
const errorObj = error as Record<string, unknown>;

if (errorObj.message) {
if (typeof errorObj.message === 'string') {
return errorObj.message;
}

if (errorObj.error?.message) {
return errorObj.error.message;
const nestedError = errorObj.error;
if (typeof nestedError === 'object' && nestedError !== null && 'message' in nestedError) {
return String((nestedError as Record<string, unknown>).message);
}

if (errorObj.error) {
return typeof errorObj.error === 'string' ? errorObj.error : JSON.stringify(errorObj.error);
if (nestedError) {
return typeof nestedError === 'string' ? nestedError : JSON.stringify(nestedError);
}

return JSON.stringify(error);
Expand All @@ -307,7 +308,7 @@ function getErrorText(error: unknown): string {
export function createErrorResponse(
error: unknown,
provider?: string,
context?: Record<string, any>
context?: Record<string, unknown>
): {
success: false;
error: string;
Expand Down Expand Up @@ -335,7 +336,7 @@ export function logError(
error: unknown,
provider?: string,
operation?: string,
additionalContext?: Record<string, any>
additionalContext?: Record<string, unknown>
): void {
const classification = classifyError(error, provider, {
operation,
Expand Down
15 changes: 13 additions & 2 deletions apps/server/src/lib/permission-enforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ export interface PermissionCheckResult {
reason?: string;
}

/** Minimal shape of a Cursor tool call used for permission checking */
interface CursorToolCall {
shellToolCall?: { args?: { command: string } };
readToolCall?: { args?: { path: string } };
writeToolCall?: { args?: { path: string } };
}
Comment on lines +15 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for CursorToolCall and related field names across the codebase
rg -n "CursorToolCall|shellToolCall|readToolCall|writeToolCall" --type ts -C 1

Repository: AutoMaker-Org/automaker

Length of output: 9614


🏁 Script executed:

#!/bin/bash
# Check the structure of `@automaker/types` package and its exports
find . -path "*/packages/types/*" -name "*.ts" -o -path "*/automaker/types/*" -name "*.ts" | head -20

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Look at the permission-enforcer.ts file to verify the current state
cat -n apps/server/src/lib/permission-enforcer.ts | head -50

Repository: AutoMaker-Org/automaker

Length of output: 1872


🏁 Script executed:

#!/bin/bash
# Check for export statements in permission-enforcer.ts
rg -n "^export" apps/server/src/lib/permission-enforcer.ts

Repository: AutoMaker-Org/automaker

Length of output: 198


Export CursorToolCall or move it to @automaker/types

The CursorToolCall interface is used as a parameter type in two exported functions (checkToolCallPermission and logPermissionViolation) but is not itself exported. This creates an ergonomic gap: callers cannot explicitly annotate variables with the type without importing it, though TypeScript's structural typing allows passing compatible objects.

More importantly, per coding guidelines ("Core TypeScript definitions with no dependencies should be isolated in the automaker/types package"), this interface should be defined in @automaker/types rather than locally. While CursorToolCallEvent already exists in the types package, the minimal shape defined here is specific to permission checking and should be extracted there and imported.

Minimal fix: Export the interface in this file, then refactor to move it to @automaker/types and import it for consistency with project conventions.

The all-optional design (allowing all three sub-call properties simultaneously) is not ideal semantically—a discriminated union would prevent invalid states—but behavior is currently correct due to priority-order checks in the implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/lib/permission-enforcer.ts` around lines 15 - 20, The
CursorToolCall interface is currently unexported but used by exported functions
checkToolCallPermission and logPermissionViolation; either export CursorToolCall
from this file now or (preferred) move its minimal definition into
`@automaker/types` and import it here (aligning with CursorToolCallEvent already
in that package). Update the local file to import the moved CursorToolCall and
adjust any references in checkToolCallPermission and logPermissionViolation to
use the shared type; keep the current all-optional shape for compatibility and
do not change the function logic.


/**
* Check if a tool call is allowed based on permissions
*/
export function checkToolCallPermission(
toolCall: any,
toolCall: CursorToolCall,
permissions: CursorCliConfigFile | null
): PermissionCheckResult {
if (!permissions || !permissions.permissions) {
Expand Down Expand Up @@ -152,7 +159,11 @@ function matchesRule(toolName: string, rule: string): boolean {
/**
* Log permission violations
*/
export function logPermissionViolation(toolCall: any, reason: string, sessionId?: string): void {
export function logPermissionViolation(
toolCall: CursorToolCall,
reason: string,
sessionId?: string
): void {
const sessionIdStr = sessionId ? ` [${sessionId}]` : '';

if (toolCall.shellToolCall?.args?.command) {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/lib/worktree-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export async function readWorktreeMetadata(
const metadataPath = getWorktreeMetadataPath(projectPath, branch);
const content = (await secureFs.readFile(metadataPath, 'utf-8')) as string;
return JSON.parse(content) as WorktreeMetadata;
} catch (error) {
} catch (_error) {
// File doesn't exist or can't be read
return null;
}
Expand Down
41 changes: 10 additions & 31 deletions apps/server/src/providers/claude-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* with the provider architecture.
*/

import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
import { query, type Options, type SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
import { BaseProvider } from './base-provider.js';
import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils';

Expand All @@ -32,31 +32,6 @@ import type {
ModelDefinition,
} from './types.js';

// Explicit allowlist of environment variables to pass to the SDK.
// Only these vars are passed - nothing else from process.env leaks through.
const ALLOWED_ENV_VARS = [
// Authentication
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
// Endpoint configuration
'ANTHROPIC_BASE_URL',
'API_TIMEOUT_MS',
// Model mappings
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
// Traffic control
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
// System vars (always from process.env)
'PATH',
'HOME',
'SHELL',
'TERM',
'USER',
'LANG',
'LC_ALL',
];

// System vars are always passed from process.env regardless of profile
const SYSTEM_ENV_VARS = ['PATH', 'HOME', 'SHELL', 'TERM', 'USER', 'LANG', 'LC_ALL'];

Expand Down Expand Up @@ -258,7 +233,7 @@ export class ClaudeProvider extends BaseProvider {
};

// Build prompt payload
let promptPayload: string | AsyncIterable<any>;
let promptPayload: string | AsyncIterable<SDKUserMessage>;

if (Array.isArray(prompt)) {
// Multi-part prompt (with images)
Expand Down Expand Up @@ -317,12 +292,16 @@ export class ClaudeProvider extends BaseProvider {
? `${userMessage}\n\nTip: If you're running multiple features in auto-mode, consider reducing concurrency (maxConcurrency setting) to avoid hitting rate limits.`
: userMessage;

const enhancedError = new Error(message);
(enhancedError as any).originalError = error;
(enhancedError as any).type = errorInfo.type;
const enhancedError = new Error(message) as Error & {
originalError: unknown;
type: string;
retryAfter?: number;
};
enhancedError.originalError = error;
enhancedError.type = errorInfo.type;

if (errorInfo.isRateLimit) {
(enhancedError as any).retryAfter = errorInfo.retryAfter;
enhancedError.retryAfter = errorInfo.retryAfter;
}

throw enhancedError;
Expand Down
23 changes: 5 additions & 18 deletions apps/server/src/providers/codex-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import type {
ModelDefinition,
} from './types.js';
import {
CODEX_MODEL_MAP,
supportsReasoningEffort,
validateBareModelId,
calculateReasoningTimeout,
Expand All @@ -56,15 +55,9 @@ const CODEX_EXEC_SUBCOMMAND = 'exec';
const CODEX_JSON_FLAG = '--json';
const CODEX_MODEL_FLAG = '--model';
const CODEX_VERSION_FLAG = '--version';
const CODEX_SANDBOX_FLAG = '--sandbox';
const CODEX_APPROVAL_FLAG = '--ask-for-approval';
const CODEX_SEARCH_FLAG = '--search';
const CODEX_OUTPUT_SCHEMA_FLAG = '--output-schema';
const CODEX_CONFIG_FLAG = '--config';
const CODEX_IMAGE_FLAG = '--image';
const CODEX_ADD_DIR_FLAG = '--add-dir';
const CODEX_SKIP_GIT_REPO_CHECK_FLAG = '--skip-git-repo-check';
const CODEX_RESUME_FLAG = 'resume';
const CODEX_REASONING_EFFORT_KEY = 'reasoning_effort';
const CODEX_YOLO_FLAG = '--dangerously-bypass-approvals-and-sandbox';
const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY';
Expand Down Expand Up @@ -106,9 +99,6 @@ const TEXT_ENCODING = 'utf-8';
*/
const CODEX_CLI_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
const CODEX_FEATURE_GENERATION_BASE_TIMEOUT_MS = 300000; // 5 minutes for feature generation
const CONTEXT_WINDOW_256K = 256000;
const MAX_OUTPUT_32K = 32000;
const MAX_OUTPUT_16K = 16000;
const SYSTEM_PROMPT_SEPARATOR = '\n\n';
const CODEX_INSTRUCTIONS_DIR = '.codex';
const CODEX_INSTRUCTIONS_SECTION = 'Codex Project Instructions';
Expand Down Expand Up @@ -758,17 +748,14 @@ export class CodexProvider extends BaseProvider {
options.cwd,
codexSettings.sandboxMode !== 'danger-full-access'
);
const resolvedSandboxMode = sandboxCheck.enabled
? codexSettings.sandboxMode
: 'danger-full-access';
if (!sandboxCheck.enabled && sandboxCheck.message) {
console.warn(`[CodexProvider] ${sandboxCheck.message}`);
}
const searchEnabled =
codexSettings.enableWebSearch || resolveSearchEnabled(resolvedAllowedTools, restrictTools);
const outputSchemaPath = await writeOutputSchemaFile(options.cwd, options.outputFormat);
await writeOutputSchemaFile(options.cwd, options.outputFormat);
const imageBlocks = codexSettings.enableImages ? extractImageBlocks(options.prompt) : [];
const imagePaths = await writeImageFiles(options.cwd, imageBlocks);
await writeImageFiles(options.cwd, imageBlocks);
const approvalPolicy =
hasMcpServers && options.mcpAutoApproveTools !== undefined
? options.mcpAutoApproveTools
Expand Down Expand Up @@ -801,7 +788,7 @@ export class CodexProvider extends BaseProvider {
overrides.push({ key: 'features.web_search_request', value: true });
}

const configOverrides = buildConfigOverrides(overrides);
buildConfigOverrides(overrides);
const preExecArgs: string[] = [];

// Add additional directories with write access
Expand Down Expand Up @@ -1033,7 +1020,7 @@ export class CodexProvider extends BaseProvider {
async detectInstallation(): Promise<InstallationStatus> {
const cliPath = await findCodexCliPath();
const hasApiKey = Boolean(await resolveOpenAiApiKey());
const authIndicators = await getCodexAuthIndicators();
await getCodexAuthIndicators();
const installed = !!cliPath;

let version = '';
Expand All @@ -1045,7 +1032,7 @@ export class CodexProvider extends BaseProvider {
cwd: process.cwd(),
});
version = result.stdout.trim();
} catch (error) {
} catch {
version = '';
}
}
Expand Down
4 changes: 0 additions & 4 deletions apps/server/src/providers/copilot-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ interface SdkToolExecutionEndEvent extends SdkEvent {
};
}

interface SdkSessionIdleEvent extends SdkEvent {
type: 'session.idle';
}

interface SdkSessionErrorEvent extends SdkEvent {
type: 'session.error';
data: {
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/providers/cursor-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ interface CursorToolHandler<TArgs = unknown, TResult = unknown> {
* Registry of Cursor tool handlers
* Each handler knows how to normalize its specific tool call type
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- handler registry stores heterogeneous tool type parameters
const CURSOR_TOOL_HANDLERS: Record<string, CursorToolHandler<any, any>> = {
readToolCall: {
name: 'Read',
Expand Down Expand Up @@ -878,7 +879,7 @@ export class CursorProvider extends CliProvider {
logger.debug(`CursorProvider.executeQuery called with model: "${options.model}"`);

// Get effective permissions for this project
const effectivePermissions = await getEffectivePermissions(options.cwd || process.cwd());
await getEffectivePermissions(options.cwd || process.cwd());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dead I/O — getEffectivePermissions result is discarded, making this call a no-op.

getEffectivePermissions performs up to two readFile operations and has no side effects. Calling it without consuming the return value adds latency to every executeQuery invocation with zero effect on behaviour.

The variable was removed as "unused", but that points to one of two problems:

  1. Permissions were never enforced — the call should be removed entirely.
  2. Enforcement logic was accidentally dropped — the permissions result should be used to gate operations (e.g., validate that file writes/shell commands are permitted before spawning the subprocess).
🐛 Option A — remove the dead call (if permissions are not yet implemented)
-    // Get effective permissions for this project
-    await getEffectivePermissions(options.cwd || process.cwd());
-
     // Debug: log raw events when AUTOMAKER_DEBUG_RAW_OUTPUT is enabled

Also remove the getEffectivePermissions import at line 34 if it becomes unused:

-import { getEffectivePermissions } from '../services/cursor-config-service.js';
🛡️ Option B — wire up permissions enforcement (if this is the intended behaviour)
-    await getEffectivePermissions(options.cwd || process.cwd());
+    const permissions = await getEffectivePermissions(options.cwd || process.cwd());
+    if (permissions) {
+      // TODO: enforce permissions before spawning subprocess
+      // e.g. reject write/shell tool calls when not allowed
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await getEffectivePermissions(options.cwd || process.cwd());
const permissions = await getEffectivePermissions(options.cwd || process.cwd());
if (permissions) {
// TODO: enforce permissions before spawning subprocess
// e.g. reject write/shell tool calls when not allowed
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/cursor-provider.ts` at line 882, The call to
getEffectivePermissions(...) inside executeQuery is dead I/O because its return
value is discarded; either remove the call and its import (if permissions are
not used) or use the returned permissions to enforce checks before proceeding.
If removing, delete the getEffectivePermissions call and remove its import; if
enforcing, assign const permissions = await getEffectivePermissions(options.cwd
|| process.cwd()) and use permissions to validate/deny file writes or shell
command spawning in executeQuery (e.g., gate the subprocess spawn or write
operations based on permissions).


// Debug: log raw events when AUTOMAKER_DEBUG_RAW_OUTPUT is enabled
const debugRawEvents =
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/providers/gemini-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import type {
ProviderMessage,
InstallationStatus,
ModelDefinition,
ContentBlock,
} from './types.js';
import { validateBareModelId } from '@automaker/types';
import { GEMINI_MODEL_MAP, type GeminiAuthStatus } from '@automaker/types';
Expand Down
Loading