From c9c291f4d82e54bbf02fbe9f964aa3645c6fb369 Mon Sep 17 00:00:00 2001 From: Maxi Date: Thu, 19 Mar 2026 11:24:36 +0000 Subject: [PATCH] chore: enabling linting --- .github/workflows/test-lint.yml | 5 +- eslint.config.js | 10 ++- package.json | 4 +- packages/cli/package.json | 2 + packages/cli/src/args.ts | 19 ++--- packages/cli/src/commands/pending.ts | 4 +- packages/cli/src/commands/pr-review.ts | 13 +++- packages/cli/src/commands/preflight.ts | 4 +- packages/cli/src/commands/register.ts | 4 +- packages/cli/src/commands/start.ts | 17 +++-- packages/cli/src/config.ts | 14 +++- packages/cli/src/index.ts | 20 ++++- packages/cli/src/process.ts | 18 ++++- packages/cli/test/logs.test.ts | 4 +- packages/common/package.json | 2 + packages/marketing/eslint.config.js | 2 +- packages/marketing/package.json | 1 + .../src/components/IntegrationsSection.tsx | 16 +++- .../marketing/src/components/ui/command.tsx | 2 +- .../marketing/src/components/ui/textarea.tsx | 2 +- packages/marketing/tailwind.config.ts | 3 +- packages/orchestrator/package.json | 2 + .../src/ai-adapters/base-adapter.ts | 12 +-- .../src/ai-adapters/claude-code-adapter.ts | 5 +- .../claude-code-event-collector.ts | 4 +- .../src/ai-adapters/codex-adapter.ts | 6 +- .../src/ai-adapters/codex-event-collector.ts | 15 ++-- .../src/ai-adapters/execution-metadata.ts | 4 +- .../src/ai-adapters/gemini-adapter.ts | 14 ++-- .../src/ai-adapters/stream-log.ts | 45 +++++------ packages/orchestrator/src/config-loader.ts | 23 +++--- packages/orchestrator/src/database.ts | 5 +- packages/orchestrator/src/git-service.ts | 9 ++- .../orchestrator/src/github/review-service.ts | 21 ++++-- packages/orchestrator/src/index.ts | 15 +++- packages/orchestrator/src/logger.ts | 16 ++-- .../src/logging/task-log-store.ts | 4 +- .../orchestrator/src/runtime/api-server.ts | 28 +++++-- .../src/runtime/api/request-parsers.ts | 12 ++- .../orchestrator/src/workflow/task-runner.ts | 18 +++-- .../test/ai-adapters/claude-code.test.ts | 13 +++- .../test/ai-adapters/codex.test.ts | 35 ++++----- packages/orchestrator/test/api-server.test.ts | 28 ++++--- .../orchestrator/test/config-loader.test.ts | 4 +- .../test/execution-metadata.test.ts | 74 +++++++++++-------- .../orchestrator/test/request-parsers.test.ts | 8 +- packages/orchestrator/test/stream-log.test.ts | 4 +- packages/orchestrator/test/ui-server.test.ts | 5 +- packages/ui/eslint.config.js | 2 +- packages/ui/package.json | 1 + 50 files changed, 381 insertions(+), 217 deletions(-) diff --git a/.github/workflows/test-lint.yml b/.github/workflows/test-lint.yml index 02e0558..1c8b6a4 100644 --- a/.github/workflows/test-lint.yml +++ b/.github/workflows/test-lint.yml @@ -28,9 +28,8 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - # Re-enable once the repo-level lint hang is resolved. - # - name: Lint - # run: pnpm lint + - name: Lint + run: pnpm lint - name: Test run: pnpm test diff --git a/eslint.config.js b/eslint.config.js index 06d996e..d7581e8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,6 +26,14 @@ export default tseslint.config( }, }, { - ignores: ['**/dist/**', '**/node_modules/**', '**/worktrees/**', '**/workspaces/**'], + ignores: [ + '**/dist/**', + '**/node_modules/**', + '**/.next/**', + '**/.vite/**', + '**/coverage/**', + '**/worktrees/**', + '**/workspaces/**', + ], } ) diff --git a/package.json b/package.json index 8cfe6a3..2248b51 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "scripts": { "build": "pnpm -r build", "test": "pnpm -r test", - "lint": "pnpm exec eslint .", - "lint:fix": "pnpm exec eslint . --fix", + "lint": "pnpm --filter './packages/*' -r lint && pnpm exec eslint eslint.config.js", + "lint:fix": "pnpm --filter './packages/*' -r lint:fix && pnpm exec eslint eslint.config.js --fix", "parallax": "NODE_ENV=dev pnpm --filter parallax-cli start", "clean": "rm -rf packages/orchestrator/parallax.db packages/orchestrator/workspaces", "release:pack": "pnpm --filter parallax-cli pack:tarball", diff --git a/packages/cli/package.json b/packages/cli/package.json index 608321b..12acce0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,6 +20,8 @@ }, "scripts": { "build": "rm -rf dist && tsc", + "lint": "eslint src test scripts vitest.config.ts", + "lint:fix": "eslint src test scripts vitest.config.ts --fix", "prepack": "node ./scripts/prepare-package.mjs", "postpack": "node ./scripts/restore-workspace-links.mjs", "pack:check": "npm_config_cache=.npm-cache npm pack --dry-run", diff --git a/packages/cli/src/args.ts b/packages/cli/src/args.ts index 1b7da0c..6a1dc56 100644 --- a/packages/cli/src/args.ts +++ b/packages/cli/src/args.ts @@ -89,8 +89,7 @@ export function parseStartOptions(args: string[]): StartCommandOptions { const apiPort = parseStrictPort(args, 'server-api-port', 3000) const uiPort = parseStrictPort(args, 'server-ui-port', 8080) const rawConcurrency = parseOptionalArg(args, 'concurrency') - const concurrency = - rawConcurrency === undefined ? 2 : Number.parseInt(rawConcurrency, 10) + const concurrency = rawConcurrency === undefined ? 2 : Number.parseInt(rawConcurrency, 10) if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 16) { throw new Error('--concurrency must be an integer between 1 and 16.') @@ -260,15 +259,13 @@ export function parseRegisterOptions( throw new Error('parallax unregister does not accept flags.') } - const positionalArgs = args - .slice(1) - .filter((entry, index, entries) => { - const previous = entries[index - 1] - if (previous === '--env-file') { - return false - } - return !entry.startsWith('--') - }) + const positionalArgs = args.slice(1).filter((entry, index, entries) => { + const previous = entries[index - 1] + if (previous === '--env-file') { + return false + } + return !entry.startsWith('--') + }) if (positionalArgs.length > 0) { throw new Error(`parallax ${command} accepts exactly one .`) } diff --git a/packages/cli/src/commands/pending.ts b/packages/cli/src/commands/pending.ts index 755fa35..8607f45 100644 --- a/packages/cli/src/commands/pending.ts +++ b/packages/cli/src/commands/pending.ts @@ -68,7 +68,9 @@ async function postJson(url: string, body: unknown) { function printPendingSummary(tasks: TaskPendingState[]) { for (const task of tasks) { - console.log(`- ${task.id} | project=${task.projectId} | plan=${task.planState} | agent=${task.lastAgent ?? 'n/a'}`) + console.log( + `- ${task.id} | project=${task.projectId} | plan=${task.planState} | agent=${task.lastAgent ?? 'n/a'}` + ) console.log(` title: ${task.title ?? '(no title)'}`) const snippet = task.planMarkdown ?? task.planResult if (snippet) { diff --git a/packages/cli/src/commands/pr-review.ts b/packages/cli/src/commands/pr-review.ts index a844fd2..1a7fee6 100644 --- a/packages/cli/src/commands/pr-review.ts +++ b/packages/cli/src/commands/pr-review.ts @@ -14,7 +14,9 @@ async function postJson(url: string, body: unknown): Promise { if (!response.ok) { const payload = (await response.json().catch(() => undefined)) as { error?: string } | undefined - throw new Error(payload?.error ?? `Request failed: ${url} ${response.status} ${response.statusText}`) + throw new Error( + payload?.error ?? `Request failed: ${url} ${response.status} ${response.statusText}` + ) } return response.json() as Promise @@ -26,12 +28,17 @@ export async function runPrReview(args: string[], context: CliContext) { console.log('') console.log(`${YELLOW}${BOLD}⚠ Experimental: pr-review is an early on-demand workflow.${RESET}`) - console.log(`${YELLOW}It will try to apply open human PR review comments to the existing PR branch.${RESET}`) + console.log( + `${YELLOW}It will try to apply open human PR review comments to the existing PR branch.${RESET}` + ) console.log('') let queuedTask: { reviewTaskId: string; prNumber: number } try { - queuedTask = await postJson(`${apiBase}/tasks/${encodeURIComponent(options.taskId)}/pr-review`, {}) + queuedTask = await postJson( + `${apiBase}/tasks/${encodeURIComponent(options.taskId)}/pr-review`, + {} + ) } catch (error) { throw new Error( `Failed to queue PR review for task ${options.taskId}: ${ diff --git a/packages/cli/src/commands/preflight.ts b/packages/cli/src/commands/preflight.ts index 61cb678..2092233 100644 --- a/packages/cli/src/commands/preflight.ts +++ b/packages/cli/src/commands/preflight.ts @@ -93,7 +93,9 @@ export async function runPreflight(args: string[]) { name: 'claude CLI', ok: claudeOk, required: false, - detail: claudeOk ? undefined : 'Install Claude Code CLI (npm i -g @anthropic-ai/claude-code).', + detail: claudeOk + ? undefined + : 'Install Claude Code CLI (npm i -g @anthropic-ai/claude-code).', }) checks.push({ diff --git a/packages/cli/src/commands/register.ts b/packages/cli/src/commands/register.ts index cc357fc..31ac203 100644 --- a/packages/cli/src/commands/register.ts +++ b/packages/cli/src/commands/register.ts @@ -20,7 +20,9 @@ async function reloadRunningRuntime(context: CliContext) { }) if (!response.ok) { const payload = (await response.json().catch(() => undefined)) as { error?: string } | undefined - throw new Error(payload?.error ?? `Failed to reload running Parallax instance (${response.status}).`) + throw new Error( + payload?.error ?? `Failed to reload running Parallax instance (${response.status}).` + ) } return true diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index c3d9469..14376b5 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -102,11 +102,7 @@ export async function runStart(args: string[], context: CliContext) { if (workspaceDevMode) { orchestratorPid = spawnDetached( process.execPath, - [ - '--import', - 'tsx', - path.resolve(context.rootDir, 'packages/orchestrator/src/index.ts'), - ], + ['--import', 'tsx', path.resolve(context.rootDir, 'packages/orchestrator/src/index.ts')], context.rootDir, env, { @@ -117,7 +113,16 @@ export async function runStart(args: string[], context: CliContext) { uiPid = spawnDetached( 'pnpm', - ['--filter', '@parallax/ui', 'start', '--', '--host', '0.0.0.0', '--port', String(options.uiPort)], + [ + '--filter', + '@parallax/ui', + 'start', + '--', + '--host', + '0.0.0.0', + '--port', + String(options.uiPort), + ], context.rootDir, { VITE_PARALLAX_API_BASE: `http://localhost:${options.apiPort}`, diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index bd7192b..065263f 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -107,7 +107,11 @@ export function parseRegistryState(raw: string, source: string): RegistryState { ) } - if (!parsed || typeof parsed !== 'object' || !Array.isArray((parsed as { configs?: unknown }).configs)) { + if ( + !parsed || + typeof parsed !== 'object' || + !Array.isArray((parsed as { configs?: unknown }).configs) + ) { throw new Error(`Invalid config registry at ${source}.`) } @@ -128,8 +132,7 @@ export function parseRegistryState(raw: string, source: string): RegistryState { return { configPath: (entry as { configPath: string }).configPath, addedAt: (entry as { addedAt: number }).addedAt, - envFilePath: - (entry as { envFilePath?: string }).envFilePath?.trim() || undefined, + envFilePath: (entry as { envFilePath?: string }).envFilePath?.trim() || undefined, } }), } @@ -165,7 +168,10 @@ export async function resolveServerPorts(configPath: string): Promise { +export async function loadRunningState( + dataDir: string, + manifestFile: string +): Promise { const manifestPath = path.join(dataDir, manifestFile) if (!(await ensureFileExists(manifestPath))) { throw new Error(`No running instance found at ${manifestPath}. Run parallax start first.`) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 1307718..48d7531 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -4,7 +4,20 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { DEFAULT_API_PORT } from '@parallax/common' -import { hasFlag, parseCancelOptions, parseLogsOptions, parsePendingOptions, parsePreflightOptions, parsePrReviewOptions, parseRegisterOptions, parseRetryOptions, parseStartOptions, parseStatusOptions, parseStopOptions as parseStopOptionsInternal, resolvePath } from './args.js' +import { + hasFlag, + parseCancelOptions, + parseLogsOptions, + parsePendingOptions, + parsePreflightOptions, + parsePrReviewOptions, + parseRegisterOptions, + parseRetryOptions, + parseStartOptions, + parseStatusOptions, + parseStopOptions as parseStopOptionsInternal, + resolvePath, +} from './args.js' import { ensureFileExists, loadRegistry as loadRegistryFromDisk, @@ -56,7 +69,10 @@ function resolvePackageVersion(rootDir: string): string { continue } - const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8')) as { version?: string; name?: string } + const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8')) as { + version?: string + name?: string + } if ( typeof parsed.version === 'string' && (parsed.name === 'parallax-cli' || candidate.endsWith('/packages/cli/package.json')) diff --git a/packages/cli/src/process.ts b/packages/cli/src/process.ts index 7af3b3e..a07a6e8 100644 --- a/packages/cli/src/process.ts +++ b/packages/cli/src/process.ts @@ -88,7 +88,11 @@ export async function waitForExit(pid: number, timeoutMs: number): Promise { +export async function stopProcessOrThrow( + pid: number, + label: string, + force: boolean +): Promise { if (!isProcessAlive(pid)) { throw new Error(`${label} process ${pid} is not running.`) } @@ -110,7 +114,11 @@ export async function stopProcessOrThrow(pid: number, label: string, force: bool } } -export async function stopProcessBestEffort(pid: number | undefined, label: string, force: boolean) { +export async function stopProcessBestEffort( + pid: number | undefined, + label: string, + force: boolean +) { if (!pid || !Number.isFinite(pid) || pid <= 0 || !isProcessAlive(pid)) { return } @@ -146,7 +154,11 @@ export async function waitForUrlHealth(url: string, name: string): Promise throw new Error(`${name} failed to become ready at ${url}: ${lastError ?? 'timeout'}`) } -export async function readFileTail(filePath: string, ensureFileExists: (filePath: string) => Promise, maxLines: number = 30) { +export async function readFileTail( + filePath: string, + ensureFileExists: (filePath: string) => Promise, + maxLines: number = 30 +) { if (!(await ensureFileExists(filePath))) { return '(log file not found)' } diff --git a/packages/cli/test/logs.test.ts b/packages/cli/test/logs.test.ts index 16dedce..ed36cf3 100644 --- a/packages/cli/test/logs.test.ts +++ b/packages/cli/test/logs.test.ts @@ -65,9 +65,7 @@ describe('runLogs', () => { vi.spyOn(Date, 'now').mockReturnValue(5_000) const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - sleepMock - .mockResolvedValueOnce(undefined) - .mockRejectedValueOnce(stopLoop) + sleepMock.mockResolvedValueOnce(undefined).mockRejectedValueOnce(stopLoop) vi.mocked(fetch) .mockResolvedValueOnce({ diff --git a/packages/common/package.json b/packages/common/package.json index 957afd4..cad8b78 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -17,6 +17,8 @@ }, "scripts": { "build": "rm -rf dist && tsc", + "lint": "eslint src test", + "lint:fix": "eslint src test --fix", "test": "vitest run" }, "devDependencies": { diff --git a/packages/marketing/eslint.config.js b/packages/marketing/eslint.config.js index 40f72cc..2fad73f 100644 --- a/packages/marketing/eslint.config.js +++ b/packages/marketing/eslint.config.js @@ -5,7 +5,7 @@ import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ["dist"] }, + { ignores: ["dist", ".next", ".vite", "coverage"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], diff --git a/packages/marketing/package.json b/packages/marketing/package.json index 0ce9932..7266807 100644 --- a/packages/marketing/package.json +++ b/packages/marketing/package.json @@ -8,6 +8,7 @@ "build": "node ./scripts/sync-docs.mjs && vite build", "build:dev": "node ./scripts/sync-docs.mjs && vite build --mode development", "lint": "eslint .", + "lint:fix": "eslint . --fix", "preview": "node ./scripts/sync-docs.mjs && vite preview", "test": "vitest run", "test:watch": "vitest" diff --git a/packages/marketing/src/components/IntegrationsSection.tsx b/packages/marketing/src/components/IntegrationsSection.tsx index 3f158c9..4e34c3a 100644 --- a/packages/marketing/src/components/IntegrationsSection.tsx +++ b/packages/marketing/src/components/IntegrationsSection.tsx @@ -1,5 +1,13 @@ +import type { ReactNode } from "react"; import { MessageSquareCode, RotateCcw } from "lucide-react"; +type IntegrationItem = { + href: string; + logo: string | ReactNode; + label: string; + imgClass?: string; +}; + const IconCard = ({ href, logo, @@ -7,7 +15,7 @@ const IconCard = ({ imgClass, }: { href: string; - logo: string | React.ReactNode; + logo: string | ReactNode; label: string; imgClass?: string; }) => ( @@ -30,7 +38,7 @@ const IconCard = ({ ); -const steps = [ +const steps: Array<{ label: string; items: IntegrationItem[] }> = [ { label: "Pulling", items: [ @@ -100,7 +108,7 @@ const IntegrationsSection = () => { href={item.href} logo={item.logo} label={item.label} - imgClass={"imgClass" in item ? (item as any).imgClass : undefined} + imgClass={item.imgClass} /> ))} @@ -130,7 +138,7 @@ const IntegrationsSection = () => { href={item.href} logo={item.logo} label={item.label} - imgClass={"imgClass" in item ? (item as any).imgClass : undefined} + imgClass={item.imgClass} /> ))} diff --git a/packages/marketing/src/components/ui/command.tsx b/packages/marketing/src/components/ui/command.tsx index 68d5378..ad32c15 100644 --- a/packages/marketing/src/components/ui/command.tsx +++ b/packages/marketing/src/components/ui/command.tsx @@ -21,7 +21,7 @@ const Command = React.forwardRef< )); Command.displayName = CommandPrimitive.displayName; -interface CommandDialogProps extends DialogProps {} +type CommandDialogProps = DialogProps; const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/packages/marketing/src/components/ui/textarea.tsx b/packages/marketing/src/components/ui/textarea.tsx index 4a5643e..18eba7f 100644 --- a/packages/marketing/src/components/ui/textarea.tsx +++ b/packages/marketing/src/components/ui/textarea.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -export interface TextareaProps extends React.TextareaHTMLAttributes {} +export type TextareaProps = React.TextareaHTMLAttributes; const Textarea = React.forwardRef(({ className, ...props }, ref) => { return ( diff --git a/packages/marketing/tailwind.config.ts b/packages/marketing/tailwind.config.ts index 0af59fc..0868639 100644 --- a/packages/marketing/tailwind.config.ts +++ b/packages/marketing/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from "tailwindcss"; +import tailwindcssAnimate from "tailwindcss-animate"; export default { darkMode: ["class"], @@ -93,5 +94,5 @@ export default { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [tailwindcssAnimate], } satisfies Config; diff --git a/packages/orchestrator/package.json b/packages/orchestrator/package.json index 727ee88..370e772 100644 --- a/packages/orchestrator/package.json +++ b/packages/orchestrator/package.json @@ -6,6 +6,8 @@ "scripts": { "start": "tsx watch src/index.ts", "build": "rm -rf dist && tsc", + "lint": "eslint src test vitest.config.ts", + "lint:fix": "eslint src test vitest.config.ts --fix", "test": "vitest run" }, "dependencies": { diff --git a/packages/orchestrator/src/ai-adapters/base-adapter.ts b/packages/orchestrator/src/ai-adapters/base-adapter.ts index 60f61e4..1a21c58 100644 --- a/packages/orchestrator/src/ai-adapters/base-adapter.ts +++ b/packages/orchestrator/src/ai-adapters/base-adapter.ts @@ -1,12 +1,6 @@ import fs from 'node:fs/promises' import dotenv from 'dotenv' -import { - Task, - Logger, - ProjectConfig, - AgentResult, - PlanResult, -} from '@parallax/common' +import { Task, Logger, ProjectConfig, AgentResult, PlanResult } from '@parallax/common' import { LocalExecutor } from '@parallax/common/executor' export abstract class BaseAgentAdapter { @@ -21,7 +15,9 @@ export abstract class BaseAgentAdapter { this.logger.info(`Workspace already prepared via git worktree: ${workingDir}`, task.id) } - protected async resolveProjectEnv(project: ProjectConfig): Promise | undefined> { + protected async resolveProjectEnv( + project: ProjectConfig + ): Promise | undefined> { if (!project.envFilePath) { return undefined } diff --git a/packages/orchestrator/src/ai-adapters/claude-code-adapter.ts b/packages/orchestrator/src/ai-adapters/claude-code-adapter.ts index 8779962..221c016 100644 --- a/packages/orchestrator/src/ai-adapters/claude-code-adapter.ts +++ b/packages/orchestrator/src/ai-adapters/claude-code-adapter.ts @@ -196,7 +196,7 @@ export class ClaudeCodeAdapter extends BaseAgentAdapter { const result = await this.executor.executeCommand(command, { cwd: workingDir, - onData: (chunk) => + onData: (chunk) => chunk.stream === 'stdout' ? collector.handleStdoutLine(chunk.line) : collector.handleStderrLine(chunk.line), @@ -283,7 +283,8 @@ export class ClaudeCodeAdapter extends BaseAgentAdapter { success: result.exitCode === 0, output: parsedOutput, ...extractExecutionMetadata(parsedOutput), - error: result.exitCode !== 0 ? `Claude Code exited with code ${result.exitCode}` : undefined, + error: + result.exitCode !== 0 ? `Claude Code exited with code ${result.exitCode}` : undefined, } } catch (error: any) { return { success: false, output: '', error: error.message } diff --git a/packages/orchestrator/src/ai-adapters/claude-code-event-collector.ts b/packages/orchestrator/src/ai-adapters/claude-code-event-collector.ts index c88a706..c025ed1 100644 --- a/packages/orchestrator/src/ai-adapters/claude-code-event-collector.ts +++ b/packages/orchestrator/src/ai-adapters/claude-code-event-collector.ts @@ -58,8 +58,8 @@ export class ClaudeCodeEventCollector { private emitBlock(args: { title: string message: string - kind: typeof TASK_LOG_KIND[keyof typeof TASK_LOG_KIND] - level?: typeof TASK_LOG_LEVEL[keyof typeof TASK_LOG_LEVEL] + kind: (typeof TASK_LOG_KIND)[keyof typeof TASK_LOG_KIND] + level?: (typeof TASK_LOG_LEVEL)[keyof typeof TASK_LOG_LEVEL] }) { this.logger.event({ taskId: this.task.id, diff --git a/packages/orchestrator/src/ai-adapters/codex-adapter.ts b/packages/orchestrator/src/ai-adapters/codex-adapter.ts index 0b9306f..2e025ec 100644 --- a/packages/orchestrator/src/ai-adapters/codex-adapter.ts +++ b/packages/orchestrator/src/ai-adapters/codex-adapter.ts @@ -416,7 +416,11 @@ export class CodexAdapter extends BaseAgentAdapter { ].join('\n') } - private buildExecutionPrompt(task: Task, approvedPlan?: string, outputMode: 'pr' | 'commit' = 'pr'): string { + private buildExecutionPrompt( + task: Task, + approvedPlan?: string, + outputMode: 'pr' | 'commit' = 'pr' + ): string { const base = `Task ID: ${task.externalId}\nTitle: ${task.title}\nDescription:\n${task.description}` const planLine = approvedPlan ? `\n\nApproved Plan:\n${approvedPlan}` : '' const outputInstruction = diff --git a/packages/orchestrator/src/ai-adapters/codex-event-collector.ts b/packages/orchestrator/src/ai-adapters/codex-event-collector.ts index 6ae2b47..7f6d85f 100644 --- a/packages/orchestrator/src/ai-adapters/codex-event-collector.ts +++ b/packages/orchestrator/src/ai-adapters/codex-event-collector.ts @@ -129,9 +129,9 @@ export class CodexEventCollector { private emitBlock(args: { title: string message: string - kind: typeof TASK_LOG_KIND[keyof typeof TASK_LOG_KIND] - level?: typeof TASK_LOG_LEVEL[keyof typeof TASK_LOG_LEVEL] - source?: typeof TASK_LOG_SOURCE[keyof typeof TASK_LOG_SOURCE] + kind: (typeof TASK_LOG_KIND)[keyof typeof TASK_LOG_KIND] + level?: (typeof TASK_LOG_LEVEL)[keyof typeof TASK_LOG_LEVEL] + source?: (typeof TASK_LOG_SOURCE)[keyof typeof TASK_LOG_SOURCE] }) { this.logger.event({ taskId: this.task.id, @@ -145,7 +145,8 @@ export class CodexEventCollector { private handleCommandExecution(item: Record) { const command = extractTextContent(item.command) ?? 'Command' - const output = extractTextContent(item.aggregated_output) ?? extractTextContent(item.output) ?? '(no output)' + const output = + extractTextContent(item.aggregated_output) ?? extractTextContent(item.output) ?? '(no output)' const exitCode = typeof item.exit_code === 'number' ? item.exit_code @@ -297,7 +298,11 @@ export class CodexEventCollector { return } - if (lowerLine.includes('error') || lowerLine.includes('failed') || lowerLine.includes('fatal')) { + if ( + lowerLine.includes('error') || + lowerLine.includes('failed') || + lowerLine.includes('fatal') + ) { this.emitBlock({ title: 'Codex stderr', message: normalized, diff --git a/packages/orchestrator/src/ai-adapters/execution-metadata.ts b/packages/orchestrator/src/ai-adapters/execution-metadata.ts index ade92da..d87f6c8 100644 --- a/packages/orchestrator/src/ai-adapters/execution-metadata.ts +++ b/packages/orchestrator/src/ai-adapters/execution-metadata.ts @@ -46,7 +46,9 @@ function extractPrSummary(output: string) { return normalized || undefined } -export function extractExecutionMetadata(output: string): Pick { +export function extractExecutionMetadata( + output: string +): Pick { const titleMatch = output.match(/PARALLAX_PR_TITLE:\s*(.+)/i) const commitMessageMatch = output.match(/PARALLAX_COMMIT_MESSAGE:\s*(.+)/i) diff --git a/packages/orchestrator/src/ai-adapters/gemini-adapter.ts b/packages/orchestrator/src/ai-adapters/gemini-adapter.ts index 597e002..93a7d90 100644 --- a/packages/orchestrator/src/ai-adapters/gemini-adapter.ts +++ b/packages/orchestrator/src/ai-adapters/gemini-adapter.ts @@ -1,4 +1,11 @@ -import { Task, ProjectConfig, AgentResult, Logger, PlanResult, PlanResultStatus } from '@parallax/common' +import { + Task, + ProjectConfig, + AgentResult, + Logger, + PlanResult, + PlanResultStatus, +} from '@parallax/common' import { BaseAgentAdapter } from './base-adapter.js' import { extractExecutionMetadata } from './execution-metadata.js' import { classifyAgentLogChunk } from './stream-log.js' @@ -64,10 +71,7 @@ export class GeminiAdapter extends BaseAgentAdapter { command.push('--prompt', prompt) - this.logger.info( - 'Gemini command profile: approval=auto_edit, sandbox=on', - task.id - ) + this.logger.info('Gemini command profile: approval=auto_edit, sandbox=on', task.id) return command } diff --git a/packages/orchestrator/src/ai-adapters/stream-log.ts b/packages/orchestrator/src/ai-adapters/stream-log.ts index 571b892..59404bb 100644 --- a/packages/orchestrator/src/ai-adapters/stream-log.ts +++ b/packages/orchestrator/src/ai-adapters/stream-log.ts @@ -40,9 +40,7 @@ function buildGroupId(kind: TaskLogKind, line: string) { function isMcpLifecycleLine(lowerLine: string) { return ( lowerLine.startsWith('mcp:') && - (lowerLine.includes('starting') || - lowerLine.includes('ready') || - lowerLine.includes('started')) + (lowerLine.includes('starting') || lowerLine.includes('ready') || lowerLine.includes('started')) ) } @@ -58,10 +56,7 @@ function isMcpWarningLine(lowerLine: string) { ) } -export function classifyAgentLogChunk( - line: string, - stream: 'stdout' | 'stderr' -): StreamLogEvent { +export function classifyAgentLogChunk(line: string, stream: 'stdout' | 'stderr'): StreamLogEvent { const normalized = line.trim() const lowerLine = normalized.toLowerCase() @@ -114,31 +109,31 @@ export function classifyAgentLogChunk( } } - return { - level: TASK_LOG_LEVEL.INFO, - kind: TASK_LOG_KIND.AGENT_MESSAGE, - source: TASK_LOG_SOURCE.AGENT, - message: normalized, - } + return { + level: TASK_LOG_LEVEL.INFO, + kind: TASK_LOG_KIND.AGENT_MESSAGE, + source: TASK_LOG_SOURCE.AGENT, + message: normalized, + } } if (isDiffLine(normalized)) { - return { - level: TASK_LOG_LEVEL.INFO, - kind: TASK_LOG_KIND.FILE_CHANGE, - source: TASK_LOG_SOURCE.AGENT, - message: normalized, + return { + level: TASK_LOG_LEVEL.INFO, + kind: TASK_LOG_KIND.FILE_CHANGE, + source: TASK_LOG_SOURCE.AGENT, + message: normalized, groupId: buildGroupId(TASK_LOG_KIND.FILE_CHANGE, normalized), - } + } } if (normalized.startsWith('$ ') || normalized.startsWith('> ')) { - return { - level: TASK_LOG_LEVEL.INFO, - kind: TASK_LOG_KIND.COMMAND, - source: TASK_LOG_SOURCE.AGENT, - message: normalized, - groupId: 'command-output', + return { + level: TASK_LOG_LEVEL.INFO, + kind: TASK_LOG_KIND.COMMAND, + source: TASK_LOG_SOURCE.AGENT, + message: normalized, + groupId: 'command-output', } } diff --git a/packages/orchestrator/src/config-loader.ts b/packages/orchestrator/src/config-loader.ts index c00cdf5..f83df98 100644 --- a/packages/orchestrator/src/config-loader.ts +++ b/packages/orchestrator/src/config-loader.ts @@ -108,11 +108,7 @@ function assertOptionalString(value: unknown, label: string): string | undefined return assertNonEmptyString(value, label) } -function assertNoUnknownKeys( - value: Record, - allowedKeys: string[], - label: string -) { +function assertNoUnknownKeys(value: Record, allowedKeys: string[], label: string) { const unknownKeys = Object.keys(value).filter((key) => !allowedKeys.includes(key)) if (unknownKeys.length > 0) { throw new Error(`${label} contains unsupported fields: ${unknownKeys.join(', ')}.`) @@ -204,7 +200,11 @@ function parseRegistry(raw: string, source: string): RegistryState { ) } - if (!parsed || typeof parsed !== 'object' || !Array.isArray((parsed as { configs?: unknown }).configs)) { + if ( + !parsed || + typeof parsed !== 'object' || + !Array.isArray((parsed as { configs?: unknown }).configs) + ) { throw new Error(`Invalid config registry at ${source}.`) } @@ -225,8 +225,7 @@ function parseRegistry(raw: string, source: string): RegistryState { return { configPath: (entry as { configPath: string }).configPath, addedAt: (entry as { addedAt: number }).addedAt, - envFilePath: - (entry as { envFilePath?: string }).envFilePath?.trim() || undefined, + envFilePath: (entry as { envFilePath?: string }).envFilePath?.trim() || undefined, } }), } @@ -267,9 +266,7 @@ function parseProject(raw: unknown, source: string): ProjectConfig { `project.agent.provider for "${id}" in ${source}` ) if ( - !ALLOWED_AGENT_PROVIDERS.includes( - agentProviderRaw as (typeof ALLOWED_AGENT_PROVIDERS)[number] - ) + !ALLOWED_AGENT_PROVIDERS.includes(agentProviderRaw as (typeof ALLOWED_AGENT_PROVIDERS)[number]) ) { throw new Error( `Unsupported agent provider "${agentProviderRaw}" for project "${id}" in ${source}. Supported providers: ${ALLOWED_AGENT_PROVIDERS.join(', ')}.` @@ -292,9 +289,7 @@ function parseProject(raw: unknown, source: string): ProjectConfig { } async function assertWorkspaceExists(project: ProjectConfig, source: string): Promise { - const stat = await fs - .stat(project.workspaceDir) - .catch(() => null) + const stat = await fs.stat(project.workspaceDir).catch(() => null) if (!stat || !stat.isDirectory()) { throw new Error( diff --git a/packages/orchestrator/src/database.ts b/packages/orchestrator/src/database.ts index 0257790..3891c83 100644 --- a/packages/orchestrator/src/database.ts +++ b/packages/orchestrator/src/database.ts @@ -329,7 +329,10 @@ export const dbService = { }, resetExecutionAttempts(id: string) { - db.prepare('UPDATE tasks SET executionAttempts = 0, updatedAt = ? WHERE id = ?').run(Date.now(), id) + db.prepare('UPDATE tasks SET executionAttempts = 0, updatedAt = ? WHERE id = ?').run( + Date.now(), + id + ) }, clearTaskPullRequestInfo(id: string) { diff --git a/packages/orchestrator/src/git-service.ts b/packages/orchestrator/src/git-service.ts index e29b6be..96a4b0b 100644 --- a/packages/orchestrator/src/git-service.ts +++ b/packages/orchestrator/src/git-service.ts @@ -238,7 +238,14 @@ export class GitService { // Ignore } - await git.raw(['worktree', 'add', '-B', localBranchName, worktreePath, `origin/${task.branchName}`]) + await git.raw([ + 'worktree', + 'add', + '-B', + localBranchName, + worktreePath, + `origin/${task.branchName}`, + ]) return worktreePath } diff --git a/packages/orchestrator/src/github/review-service.ts b/packages/orchestrator/src/github/review-service.ts index 4e19726..254e192 100644 --- a/packages/orchestrator/src/github/review-service.ts +++ b/packages/orchestrator/src/github/review-service.ts @@ -121,9 +121,7 @@ export class GitHubReviewService { const owner = payload.owner?.login?.trim() const repo = payload.name?.trim() if (!owner || !repo) { - throw new Error( - `Unable to resolve GitHub repository from workspace ${project.workspaceDir}.` - ) + throw new Error(`Unable to resolve GitHub repository from workspace ${project.workspaceDir}.`) } return { owner, repo } @@ -135,7 +133,17 @@ export class GitHubReviewService { variables: Record ): Promise { const { owner, repo } = await this.resolveRepoDetails(project) - const command = ['gh', 'api', 'graphql', '-f', `query=${query}`, '-F', `owner=${owner}`, '-F', `repo=${repo}`] + const command = [ + 'gh', + 'api', + 'graphql', + '-f', + `query=${query}`, + '-F', + `owner=${owner}`, + '-F', + `repo=${repo}`, + ] for (const [key, value] of Object.entries(variables)) { command.push('-F', `${key}=${String(value)}`) @@ -196,7 +204,10 @@ export class GitHubReviewService { } } - async listOpenReviewComments(project: ProjectConfig, prNumber: number): Promise { + async listOpenReviewComments( + project: ProjectConfig, + prNumber: number + ): Promise { const data = await this.executeGraphql( project, ` diff --git a/packages/orchestrator/src/index.ts b/packages/orchestrator/src/index.ts index d0fe3ca..724dd32 100644 --- a/packages/orchestrator/src/index.ts +++ b/packages/orchestrator/src/index.ts @@ -3,7 +3,14 @@ import { Server as SocketServer } from 'socket.io' import { dbService } from './database.js' import { GitService } from './git-service.js' import { BaseAgentAdapter } from './ai-adapters/index.js' -import { ProjectConfig, TASK_REVIEW_STATE, TASK_STATUS, TaskPlanState, type Task, sleep } from '@parallax/common' +import { + ProjectConfig, + TASK_REVIEW_STATE, + TASK_STATUS, + TaskPlanState, + type Task, + sleep, +} from '@parallax/common' import { loadConfig } from './config-loader.js' import { logger, setIo, setLogLevels } from './logger.js' import { HostExecutor } from '@parallax/common/executor' @@ -166,7 +173,6 @@ async function pollProjects( } }) } - } catch (projectError: any) { logger.error(`Project poll error (${project.id}): ${projectError.message}`) } @@ -257,7 +263,10 @@ async function main() { throw new Error(`Task ${sourceTask.id} does not have a related open PR.`) } - const pullRequestDetails = await reviewService.getPullRequestDetails(project, sourceTask.prNumber) + const pullRequestDetails = await reviewService.getPullRequestDetails( + project, + sourceTask.prNumber + ) if (pullRequestDetails.state !== 'OPEN') { throw new Error(`Task ${sourceTask.id} does not have a related open PR.`) } diff --git a/packages/orchestrator/src/logger.ts b/packages/orchestrator/src/logger.ts index ebd49c9..f00f70e 100644 --- a/packages/orchestrator/src/logger.ts +++ b/packages/orchestrator/src/logger.ts @@ -23,12 +23,7 @@ import { touchTaskStatus, updateTaskStatus, } from './logging/task-log-store.js' -import { - emitTaskLog, - emitTaskRemoved, - emitTaskStatus, - setIo, -} from './logging/socket-publisher.js' +import { emitTaskLog, emitTaskRemoved, emitTaskStatus, setIo } from './logging/socket-publisher.js' let currentLogLevels: LogLevel[] = ['info', 'success', 'warn', 'error'] @@ -80,7 +75,9 @@ function setTaskRuntimeStatus(taskId: string, message: string, status: TaskRunti } function normalizeMessage(message: string) { - return stripAnsi(message).trim().replace(/[\uFFFD]/g, '') + return stripAnsi(message) + .trim() + .replace(/[\uFFFD]/g, '') } function formatConsoleLine(taskId: string | undefined, icon: string, message: string) { @@ -167,7 +164,10 @@ function emitLoggerMessage( return } - const lines = normalizeMessage(message).split('\n').map((line) => line.trim()).filter(Boolean) + const lines = normalizeMessage(message) + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) for (const line of lines) { writeConsoleAndTaskLog(taskId, undefined, persistedLevel, icon, line, kind, source) } diff --git a/packages/orchestrator/src/logging/task-log-store.ts b/packages/orchestrator/src/logging/task-log-store.ts index bb8f289..8f61c3f 100644 --- a/packages/orchestrator/src/logging/task-log-store.ts +++ b/packages/orchestrator/src/logging/task-log-store.ts @@ -16,7 +16,9 @@ function escapeForRegex(value: string) { function canonicalizeMessage(entry: TaskLogEntry) { const withoutTaskPrefix = entry.message.replace(/^\[[^\]]+\]\s*/, '').trim() - const withoutIcon = withoutTaskPrefix.replace(new RegExp(`^${escapeForRegex(entry.icon)}\\s+`), '').trim() + const withoutIcon = withoutTaskPrefix + .replace(new RegExp(`^${escapeForRegex(entry.icon)}\\s+`), '') + .trim() return withoutIcon } diff --git a/packages/orchestrator/src/runtime/api-server.ts b/packages/orchestrator/src/runtime/api-server.ts index 338f239..9849ad7 100644 --- a/packages/orchestrator/src/runtime/api-server.ts +++ b/packages/orchestrator/src/runtime/api-server.ts @@ -86,7 +86,9 @@ export async function createApiServer( try { return await readOrchestratorErrors() } catch (error) { - return reply.status(500).send({ error: error instanceof Error ? error.message : String(error) }) + return reply + .status(500) + .send({ error: error instanceof Error ? error.message : String(error) }) } }) @@ -96,7 +98,9 @@ export async function createApiServer( emitConfigUpdated() return { ok: true, projectCount: config.projects.length } } catch (error) { - return reply.status(400).send({ error: error instanceof Error ? error.message : String(error) }) + return reply + .status(400) + .send({ error: error instanceof Error ? error.message : String(error) }) } }) @@ -116,7 +120,9 @@ export async function createApiServer( }), } } catch (error) { - return reply.status(400).send({ error: error instanceof Error ? error.message : String(error) }) + return reply + .status(400) + .send({ error: error instanceof Error ? error.message : String(error) }) } }) @@ -131,7 +137,9 @@ export async function createApiServer( try { project = resolveTaskProject(getConfig(), task.projectId) } catch (error) { - return reply.status(404).send({ error: error instanceof Error ? error.message : String(error) }) + return reply + .status(404) + .send({ error: error instanceof Error ? error.message : String(error) }) } const liveWorktree = activeWorktrees.get(task.id) @@ -164,7 +172,9 @@ export async function createApiServer( try { project = resolveTaskProject(getConfig(), task.projectId) } catch (error) { - return reply.status(404).send({ error: error instanceof Error ? error.message : String(error) }) + return reply + .status(404) + .send({ error: error instanceof Error ? error.message : String(error) }) } const liveWorktree = activeWorktrees.get(task.id) @@ -257,7 +267,9 @@ export async function createApiServer( try { mode = parseRetryMode(rawMode) } catch (error) { - return reply.status(400).send({ error: error instanceof Error ? error.message : String(error) }) + return reply + .status(400) + .send({ error: error instanceof Error ? error.message : String(error) }) } if (mode === 'execution') { @@ -330,7 +342,9 @@ export async function createApiServer( prNumber: queued.prNumber, }) } catch (error) { - return reply.status(400).send({ error: error instanceof Error ? error.message : String(error) }) + return reply + .status(400) + .send({ error: error instanceof Error ? error.message : String(error) }) } }) diff --git a/packages/orchestrator/src/runtime/api/request-parsers.ts b/packages/orchestrator/src/runtime/api/request-parsers.ts index 34bd17c..4986e2b 100644 --- a/packages/orchestrator/src/runtime/api/request-parsers.ts +++ b/packages/orchestrator/src/runtime/api/request-parsers.ts @@ -12,7 +12,11 @@ export function parseRetryMode(value: string | undefined): RetryMode { throw new Error(`Invalid retry mode '${value}'. Use 'full' or 'execution'.`) } -export function parseNonNegativeInteger(value: string | undefined, label: string, defaultValue: number) { +export function parseNonNegativeInteger( + value: string | undefined, + label: string, + defaultValue: number +) { if (value === undefined) { return defaultValue } @@ -25,7 +29,11 @@ export function parseNonNegativeInteger(value: string | undefined, label: string return parsed } -export function parsePositiveInteger(value: string | undefined, label: string, defaultValue: number) { +export function parsePositiveInteger( + value: string | undefined, + label: string, + defaultValue: number +) { if (value === undefined) { return defaultValue } diff --git a/packages/orchestrator/src/workflow/task-runner.ts b/packages/orchestrator/src/workflow/task-runner.ts index c1bb779..2077842 100644 --- a/packages/orchestrator/src/workflow/task-runner.ts +++ b/packages/orchestrator/src/workflow/task-runner.ts @@ -13,7 +13,12 @@ import { TASK_STATUS, TASK_REVIEW_STATE, } from '@parallax/common' -import { BaseAgentAdapter, ClaudeCodeAdapter, CodexAdapter, GeminiAdapter } from '../ai-adapters/index.js' +import { + BaseAgentAdapter, + ClaudeCodeAdapter, + CodexAdapter, + GeminiAdapter, +} from '../ai-adapters/index.js' import { HostExecutor } from '@parallax/common/executor' import { GitService } from '../git-service.js' import { @@ -364,7 +369,10 @@ export async function processPullRequestReview( project, comments.map((comment) => comment.threadId) ) - logger.success(`Resolved ${new Set(comments.map((comment) => comment.threadId)).size} review thread(s).`, task.id) + logger.success( + `Resolved ${new Set(comments.map((comment) => comment.threadId)).size} review thread(s).`, + task.id + ) } catch (error: any) { logger.warn(`Pushed changes but could not resolve review threads: ${error.message}`, task.id) } @@ -404,8 +412,4 @@ async function emitWorktreeDiffLogs(task: Task, gitService: GitService, worktree } } -export { - isTaskExecutable, - normalizePlanState, - requiresPlan, -} +export { isTaskExecutable, normalizePlanState, requiresPlan } diff --git a/packages/orchestrator/test/ai-adapters/claude-code.test.ts b/packages/orchestrator/test/ai-adapters/claude-code.test.ts index 6150132..e7a7f41 100644 --- a/packages/orchestrator/test/ai-adapters/claude-code.test.ts +++ b/packages/orchestrator/test/ai-adapters/claude-code.test.ts @@ -118,7 +118,11 @@ describe('ClaudeCodeAdapter', () => { } const adapter = new ClaudeCodeAdapter(mockExecutor as any, mockLogger as any) - const task = { externalId: 'REV-404', title: 'Auth refresh', description: 'Refresh flow' } as any + const task = { + externalId: 'REV-404', + title: 'Auth refresh', + description: 'Refresh flow', + } as any const project = { agent: { model: 'sonnet' } } as any const result = await adapter.runPlan(task, '/tmp/repo', project) @@ -170,7 +174,12 @@ describe('ClaudeCodeAdapter', () => { } const adapter = new ClaudeCodeAdapter(mockExecutor as any, localLogger as any) - const task = { id: 'task-405', externalId: 'REV-405', title: 'Review', description: 'Desc' } as any + const task = { + id: 'task-405', + externalId: 'REV-405', + title: 'Review', + description: 'Desc', + } as any const project = { agent: { model: 'sonnet' } } as any vi.spyOn(adapter, 'setupWorkspace').mockResolvedValue() diff --git a/packages/orchestrator/test/ai-adapters/codex.test.ts b/packages/orchestrator/test/ai-adapters/codex.test.ts index 4ffbf9e..4749680 100644 --- a/packages/orchestrator/test/ai-adapters/codex.test.ts +++ b/packages/orchestrator/test/ai-adapters/codex.test.ts @@ -86,13 +86,11 @@ describe('CodexAdapter plan mode', () => { it('builds codex execution command with model and extra args', async () => { const mockExecutor = { - executeCommand: vi - .fn() - .mockResolvedValue({ - exitCode: 0, - output: - 'done\nPARALLAX_PR_TITLE: Improve auth loading\nPARALLAX_PR_SUMMARY:\n- Updated auth flow', - }), + executeCommand: vi.fn().mockResolvedValue({ + exitCode: 0, + output: + 'done\nPARALLAX_PR_TITLE: Improve auth loading\nPARALLAX_PR_SUMMARY:\n- Updated auth flow', + }), } const adapter = new CodexAdapter(mockExecutor as any, mockLogger as any) @@ -108,14 +106,7 @@ describe('CodexAdapter plan mode', () => { const command = mockExecutor.executeCommand.mock.calls[0][0] expect(command).toEqual( - expect.arrayContaining([ - 'codex', - 'exec', - '--model', - 'codex-fast', - '--full-auto', - '--', - ]) + expect.arrayContaining(['codex', 'exec', '--model', 'codex-fast', '--full-auto', '--']) ) expect(command[command.length - 1]).toContain('You are executing an implementation plan.') expect(result.prTitle).toBe('Improve auth loading') @@ -124,9 +115,10 @@ describe('CodexAdapter plan mode', () => { it('extracts commit message for review execution output', async () => { const mockExecutor = { - executeCommand: vi - .fn() - .mockResolvedValue({ exitCode: 0, output: 'done\nPARALLAX_COMMIT_MESSAGE: Address review comments' }), + executeCommand: vi.fn().mockResolvedValue({ + exitCode: 0, + output: 'done\nPARALLAX_COMMIT_MESSAGE: Address review comments', + }), } const adapter = new CodexAdapter(mockExecutor as any, mockLogger as any) @@ -144,7 +136,12 @@ describe('CodexAdapter plan mode', () => { } const adapter = new CodexAdapter(mockExecutor as any, mockLogger as any) - const task = { id: 'task-200', externalId: 'REV-200', title: 'Execution', description: 'Do work' } as any + const task = { + id: 'task-200', + externalId: 'REV-200', + title: 'Execution', + description: 'Do work', + } as any const project = { agent: {} } as any await adapter.runTask(task, '/tmp/repo', project, 'approved plan') diff --git a/packages/orchestrator/test/api-server.test.ts b/packages/orchestrator/test/api-server.test.ts index eb603cd..73aba33 100644 --- a/packages/orchestrator/test/api-server.test.ts +++ b/packages/orchestrator/test/api-server.test.ts @@ -116,7 +116,9 @@ describe('GET /tasks', () => { server = await createApiServer(buildDependencies()) }) - afterEach(async () => { await server.close() }) + afterEach(async () => { + await server.close() + }) it('returns empty array when no tasks', async () => { const res = await server.inject({ method: 'GET', url: '/tasks' }) @@ -132,7 +134,9 @@ describe('GET /logs', () => { server = await createApiServer(buildDependencies()) }) - afterEach(async () => { await server.close() }) + afterEach(async () => { + await server.close() + }) it('returns 200 with default params', async () => { const res = await server.inject({ method: 'GET', url: '/logs' }) @@ -162,7 +166,9 @@ describe('GET /tasks/:taskId/diff/files', () => { server = await createApiServer(buildDependencies()) }) - afterEach(async () => { await server.close() }) + afterEach(async () => { + await server.close() + }) it('returns 404 when task not found', async () => { const res = await server.inject({ method: 'GET', url: '/tasks/nonexistent/diff/files' }) @@ -180,7 +186,9 @@ describe('POST /tasks/:taskId/approve', () => { server = await createApiServer(buildDependencies()) }) - afterEach(async () => { await server.close() }) + afterEach(async () => { + await server.close() + }) it('returns 404 when task not found', async () => { vi.mocked(dbService.getTaskByLookup).mockReturnValue(null) @@ -232,7 +240,9 @@ describe('POST /tasks/:taskId/retry', () => { server = await createApiServer(buildDependencies()) }) - afterEach(async () => { await server.close() }) + afterEach(async () => { + await server.close() + }) it('returns 404 when task not found', async () => { vi.mocked(dbService.getTaskByLookup).mockReturnValue(null) @@ -250,9 +260,7 @@ describe('POST /tasks/:taskId/retry', () => { }) it('returns 400 for invalid retry mode', async () => { - vi.mocked(dbService.getTaskByLookup).mockReturnValue( - makeTask({ status: TASK_STATUS.FAILED }) - ) + vi.mocked(dbService.getTaskByLookup).mockReturnValue(makeTask({ status: TASK_STATUS.FAILED })) const res = await server.inject({ method: 'POST', url: '/tasks/task-1/retry', @@ -272,7 +280,9 @@ describe('POST /tasks/:taskId/cancel', () => { server = await createApiServer(buildDependencies()) }) - afterEach(async () => { await server.close() }) + afterEach(async () => { + await server.close() + }) it('returns 404 when task not found', async () => { vi.mocked(dbService.getTaskByLookup).mockReturnValue(null) diff --git a/packages/orchestrator/test/config-loader.test.ts b/packages/orchestrator/test/config-loader.test.ts index 6b5e6fd..59a682b 100644 --- a/packages/orchestrator/test/config-loader.test.ts +++ b/packages/orchestrator/test/config-loader.test.ts @@ -221,8 +221,6 @@ describe('config-loader', () => { process.env.PARALLAX_DATA_DIR = dataDir process.chdir(root) - await expect(loadConfig()).rejects.toThrow( - 'project.agent for "test" in' - ) + await expect(loadConfig()).rejects.toThrow('project.agent for "test" in') }) }) diff --git a/packages/orchestrator/test/execution-metadata.test.ts b/packages/orchestrator/test/execution-metadata.test.ts index 6b20038..6156637 100644 --- a/packages/orchestrator/test/execution-metadata.test.ts +++ b/packages/orchestrator/test/execution-metadata.test.ts @@ -8,14 +8,16 @@ import { describe('execution metadata', () => { it('extracts PR title, PR summary, and commit message from agent output', () => { - const metadata = extractExecutionMetadata([ - 'Work complete', - 'PARALLAX_PR_TITLE: Improve dashboard loading ', - 'PARALLAX_PR_SUMMARY:', - '- Reduced N+1 queries', - '- Added loading states', - 'PARALLAX_COMMIT_MESSAGE: Tighten dashboard loading ', - ].join('\n')) + const metadata = extractExecutionMetadata( + [ + 'Work complete', + 'PARALLAX_PR_TITLE: Improve dashboard loading ', + 'PARALLAX_PR_SUMMARY:', + '- Reduced N+1 queries', + '- Added loading states', + 'PARALLAX_COMMIT_MESSAGE: Tighten dashboard loading ', + ].join('\n') + ) expect(metadata.prTitle).toBe('Improve dashboard loading') expect(metadata.prSummary).toBe('- Reduced N+1 queries\n- Added loading states') @@ -33,34 +35,42 @@ describe('execution metadata', () => { }) it('strips diff and code noise from PR summaries', () => { - const summary = normalizePrSummary([ - 'Removes the default-model field from persona saves and adds a reusable model selector.', - '- Updates request handlers so actions use the selected model.', - 'diff --git a/components/ai/ModelSelect.vue b/components/ai/ModelSelect.vue', - '+++ b/components/ai/ModelSelect.vue', - '@@ -0,0 +1,37 @@', - ].join('\n')) + const summary = normalizePrSummary( + [ + 'Removes the default-model field from persona saves and adds a reusable model selector.', + '- Updates request handlers so actions use the selected model.', + 'diff --git a/components/ai/ModelSelect.vue b/components/ai/ModelSelect.vue', + '+++ b/components/ai/ModelSelect.vue', + '@@ -0,0 +1,37 @@', + ].join('\n') + ) - expect(summary).toBe([ - 'Removes the default-model field from persona saves and adds a reusable model selector.', - '- Updates request handlers so actions use the selected model.', - ].join('\n')) + expect(summary).toBe( + [ + 'Removes the default-model field from persona saves and adds a reusable model selector.', + '- Updates request handlers so actions use the selected model.', + ].join('\n') + ) }) it('preserves prose summaries while dropping trailing noise', () => { - const summary = normalizePrSummary([ - 'Removes the default-model field from persona/onboarding saves and adds a lightweight reusable model dropdown to AI module entry points.', - '- Threads an optional model through request schemas and API handlers so each action uses the selected model.', - '- Keeps legacy profiles falling back to stored default_model during migration.', - '- Extra detail that should never appear in the final PR summary.', - 'diff --git a/components/ai/ModelSelect.vue b/components/ai/ModelSelect.vue', - ].join('\n')) + const summary = normalizePrSummary( + [ + 'Removes the default-model field from persona/onboarding saves and adds a lightweight reusable model dropdown to AI module entry points.', + '- Threads an optional model through request schemas and API handlers so each action uses the selected model.', + '- Keeps legacy profiles falling back to stored default_model during migration.', + '- Extra detail that should never appear in the final PR summary.', + 'diff --git a/components/ai/ModelSelect.vue b/components/ai/ModelSelect.vue', + ].join('\n') + ) - expect(summary).toBe([ - 'Removes the default-model field from persona/onboarding saves and adds a lightweight reusable model dropdown to AI module entry points.', - '- Threads an optional model through request schemas and API handlers so each action uses the selected model.', - '- Keeps legacy profiles falling back to stored default_model during migration.', - '- Extra detail that should never appear in the final PR summary.', - ].join('\n')) + expect(summary).toBe( + [ + 'Removes the default-model field from persona/onboarding saves and adds a lightweight reusable model dropdown to AI module entry points.', + '- Threads an optional model through request schemas and API handlers so each action uses the selected model.', + '- Keeps legacy profiles falling back to stored default_model during migration.', + '- Extra detail that should never appear in the final PR summary.', + ].join('\n') + ) }) }) diff --git a/packages/orchestrator/test/request-parsers.test.ts b/packages/orchestrator/test/request-parsers.test.ts index d3a7a53..68c497a 100644 --- a/packages/orchestrator/test/request-parsers.test.ts +++ b/packages/orchestrator/test/request-parsers.test.ts @@ -40,7 +40,9 @@ describe('parseNonNegativeInteger', () => { }) it('throws for negative values', () => { - expect(() => parseNonNegativeInteger('-1', 'since', 0)).toThrow('since must be a non-negative integer') + expect(() => parseNonNegativeInteger('-1', 'since', 0)).toThrow( + 'since must be a non-negative integer' + ) }) it('throws for non-numeric strings', () => { @@ -64,7 +66,9 @@ describe('parsePositiveInteger', () => { }) it('throws for zero', () => { - expect(() => parsePositiveInteger('0', 'limit', 200)).toThrow('limit must be a positive integer') + expect(() => parsePositiveInteger('0', 'limit', 200)).toThrow( + 'limit must be a positive integer' + ) }) it('throws for negative values', () => { diff --git a/packages/orchestrator/test/stream-log.test.ts b/packages/orchestrator/test/stream-log.test.ts index e0fe6e8..9a52139 100644 --- a/packages/orchestrator/test/stream-log.test.ts +++ b/packages/orchestrator/test/stream-log.test.ts @@ -23,7 +23,9 @@ describe('classifyAgentLogChunk', () => { }) it('does not classify generic stderr chatter as hard errors', () => { - expect(classifyAgentLogChunk('Second line should be: SUMMARY: ', 'stderr')).toEqual({ + expect( + classifyAgentLogChunk('Second line should be: SUMMARY: ', 'stderr') + ).toEqual({ level: TASK_LOG_LEVEL.INFO, kind: TASK_LOG_KIND.AGENT_MESSAGE, source: TASK_LOG_SOURCE.AGENT, diff --git a/packages/orchestrator/test/ui-server.test.ts b/packages/orchestrator/test/ui-server.test.ts index d471c31..fc1f384 100644 --- a/packages/orchestrator/test/ui-server.test.ts +++ b/packages/orchestrator/test/ui-server.test.ts @@ -5,7 +5,10 @@ import path from 'node:path' * Tests for the path traversal prevention logic in the UI server. * We test the resolution logic directly rather than spinning up the server. */ -function resolveAndValidate(uiDistPath: string, requestPath: string): { ok: boolean; resolved?: string } { +function resolveAndValidate( + uiDistPath: string, + requestPath: string +): { ok: boolean; resolved?: string } { const normalized = requestPath.startsWith('/') ? requestPath.slice(1) : requestPath const decoded = decodeURIComponent(normalized) const uiRootResolved = path.resolve(uiDistPath) diff --git a/packages/ui/eslint.config.js b/packages/ui/eslint.config.js index 40f72cc..ad9abbd 100644 --- a/packages/ui/eslint.config.js +++ b/packages/ui/eslint.config.js @@ -5,7 +5,7 @@ import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ["dist"] }, + { ignores: ["dist", ".vite", "coverage"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], diff --git a/packages/ui/package.json b/packages/ui/package.json index 854264f..a1dc1a0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -9,6 +9,7 @@ "build": "tsc && vite build", "test": "vitest run", "lint": "eslint .", + "lint:fix": "eslint . --fix", "preview": "vite preview" }, "devDependencies": {