From 3a81e28d58ba5f31bb3a0cd7afe7356be72a57ea Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sat, 27 Dec 2025 16:51:18 +0000 Subject: [PATCH 1/6] fix: update phase logs when task is stopped When a task is stopped, the task status changes to 'backlog' and execution progress resets to 'idle', but the task_logs.json files were not being updated. This caused the logs tab to continue showing "Running" badges for phases that were still marked as 'active' in the log files. Changes: - Add markActivePhasesStopped() method to TaskLogService - Update both main and worktree task_logs.json files - Mark active phases as 'failed' (interrupted) when task is stopped - Call this method in TASK_STOP handler before sending status change event - Emit logs-changed event to update UI immediately Fixes issue where logs tab shows task as running after it's been stopped. --- .../ipc-handlers/task/execution-handlers.ts | 9 +++ apps/frontend/src/main/task-log-service.ts | 67 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts index 547ea3db60..ab2b3dc01b 100644 --- a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts @@ -184,6 +184,15 @@ export function registerTaskExecutionHandlers( agentManager.killTask(taskId); fileWatcher.unwatch(taskId); + // Find task and project to update phase logs + const { task, project } = findTaskAndProject(taskId); + if (task && project) { + // Mark any active phases as stopped in the log files + const { taskLogService } = require('../../task-log-service'); + const specsBaseDir = getSpecsDir(project.autoBuildPath); + taskLogService.markActivePhasesStopped(task.specId, project.path, specsBaseDir); + } + const mainWindow = getMainWindow(); if (mainWindow) { mainWindow.webContents.send( diff --git a/apps/frontend/src/main/task-log-service.ts b/apps/frontend/src/main/task-log-service.ts index 9ad2569649..5d71f897a2 100644 --- a/apps/frontend/src/main/task-log-service.ts +++ b/apps/frontend/src/main/task-log-service.ts @@ -1,5 +1,5 @@ import path from 'path'; -import { existsSync, readFileSync, watchFile } from 'fs'; +import { existsSync, readFileSync, writeFileSync, watchFile } from 'fs'; import { EventEmitter } from 'events'; import type { TaskLogs, TaskLogPhase, TaskLogStreamChunk, TaskPhaseLog } from '../shared/types'; @@ -376,6 +376,71 @@ export class TaskLogService extends EventEmitter { const logFile = path.join(specDir, 'task_logs.json'); return existsSync(logFile); } + + /** + * Mark all active phases as stopped when a task is interrupted + * Updates both main and worktree log files if they exist + * + * @param specId - The spec ID + * @param projectPath - Project root path (needed to find worktree) + * @param specsRelPath - Relative path to specs (e.g., ".auto-claude/specs") + */ + markActivePhasesStopped(specId: string, projectPath: string, specsRelPath: string): void { + // Get watched paths for this spec + const watchedInfo = this.watchedPaths.get(specId); + + // Build paths for main and worktree spec directories + const mainSpecDir = watchedInfo?.mainSpecDir || path.join(projectPath, specsRelPath, specId); + const worktreeSpecDir = watchedInfo?.worktreeSpecDir || path.join(projectPath, '.worktrees', specId, specsRelPath, specId); + + // Update main spec dir logs + this.updateLogFilePhaseStatus(path.join(mainSpecDir, 'task_logs.json')); + + // Update worktree spec dir logs if it exists + if (existsSync(worktreeSpecDir)) { + this.updateLogFilePhaseStatus(path.join(worktreeSpecDir, 'task_logs.json')); + } + + // Reload and emit updated logs + const updatedLogs = this.loadLogs(mainSpecDir, projectPath, specsRelPath, specId); + if (updatedLogs) { + this.logCache.set(mainSpecDir, updatedLogs); + this.emit('logs-changed', specId, updatedLogs); + } + } + + /** + * Update a single log file to mark active phases as stopped + */ + private updateLogFilePhaseStatus(logFilePath: string): void { + if (!existsSync(logFilePath)) return; + + try { + const content = readFileSync(logFilePath, 'utf-8'); + const logs = JSON.parse(content) as TaskLogs; + + let hasChanges = false; + const phases: TaskLogPhase[] = ['planning', 'coding', 'validation']; + + for (const phase of phases) { + if (logs.phases[phase]?.status === 'active') { + // Mark as failed since the phase was interrupted/stopped + logs.phases[phase].status = 'failed'; + logs.phases[phase].completed_at = new Date().toISOString(); + hasChanges = true; + } + } + + // Only write if we made changes + if (hasChanges) { + logs.updated_at = new Date().toISOString(); + writeFileSync(logFilePath, JSON.stringify(logs, null, 2), 'utf-8'); + console.log(`[TaskLogService] Marked active phases as stopped in ${logFilePath}`); + } + } catch (error) { + console.error(`[TaskLogService] Failed to update log file ${logFilePath}:`, error); + } + } } // Singleton instance From fbdfad6cb48eb9d5384c4c11364895722b67d9c2 Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sat, 27 Dec 2025 16:56:20 +0000 Subject: [PATCH 2/6] fix: wrap phase log update in try-catch to ensure task stops even if log update fails The stop button wasn't working because an error in the phase log update code was preventing the task from stopping. Now the log update is wrapped in a try-catch block so the task will always stop, even if updating the logs fails. This ensures the critical stop functionality works while still attempting to update the phase logs when possible. --- .../ipc-handlers/task/execution-handlers.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts index ab2b3dc01b..822468eee8 100644 --- a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts @@ -184,13 +184,19 @@ export function registerTaskExecutionHandlers( agentManager.killTask(taskId); fileWatcher.unwatch(taskId); - // Find task and project to update phase logs - const { task, project } = findTaskAndProject(taskId); - if (task && project) { - // Mark any active phases as stopped in the log files - const { taskLogService } = require('../../task-log-service'); - const specsBaseDir = getSpecsDir(project.autoBuildPath); - taskLogService.markActivePhasesStopped(task.specId, project.path, specsBaseDir); + // Update phase logs to mark active phases as stopped + // Wrapped in try-catch to ensure task stopping still works even if log update fails + try { + const { task, project } = findTaskAndProject(taskId); + if (task && project) { + // Mark any active phases as stopped in the log files + const { taskLogService } = require('../../task-log-service'); + const specsBaseDir = getSpecsDir(project.autoBuildPath); + taskLogService.markActivePhasesStopped(task.specId, project.path, specsBaseDir); + } + } catch (error) { + // Log error but don't prevent task from stopping + console.error('[TASK_STOP] Failed to update phase logs:', error); } const mainWindow = getMainWindow(); From 3c0f2293fb48d4b1bfc538397133779a06f7c76c Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sat, 27 Dec 2025 16:57:09 +0000 Subject: [PATCH 3/6] refactor: use proper import instead of require for taskLogService Replace dynamic require() call with static import at the top of the file. This is cleaner and avoids potential module resolution issues. --- apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts index 822468eee8..4f25ecfaff 100644 --- a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts @@ -9,6 +9,7 @@ import { fileWatcher } from '../../file-watcher'; import { findTaskAndProject } from './shared'; import { checkGitStatus } from '../../project-initializer'; import { getClaudeProfileManager } from '../../claude-profile-manager'; +import { taskLogService } from '../../task-log-service'; /** * Helper function to check subtask completion status @@ -190,7 +191,6 @@ export function registerTaskExecutionHandlers( const { task, project } = findTaskAndProject(taskId); if (task && project) { // Mark any active phases as stopped in the log files - const { taskLogService } = require('../../task-log-service'); const specsBaseDir = getSpecsDir(project.autoBuildPath); taskLogService.markActivePhasesStopped(task.specId, project.path, specsBaseDir); } From bc9cf3926e560670ed25341ccc2026a3756db668 Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sat, 27 Dec 2025 17:01:53 +0000 Subject: [PATCH 4/6] refactor: use 'stopped' status instead of 'failed' for interrupted phases When a task is stopped by the user, phases are now marked as 'stopped' instead of 'failed'. This is more semantically correct and clearer to users. Changes: - Add 'stopped' to TaskLogPhaseStatus type - Update task-log-service to mark phases as 'stopped' when task is stopped - Add 'Stopped' badge in TaskLogs UI with muted styling - Add stopped status styling for phase sections The 'stopped' status uses muted colors to distinguish it from errors ('failed') while still indicating the phase didn't complete normally. --- apps/frontend/src/main/task-log-service.ts | 4 ++-- .../src/renderer/components/task-detail/TaskLogs.tsx | 8 ++++++++ apps/frontend/src/shared/types/task.ts | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/main/task-log-service.ts b/apps/frontend/src/main/task-log-service.ts index 5d71f897a2..f3f6d99d88 100644 --- a/apps/frontend/src/main/task-log-service.ts +++ b/apps/frontend/src/main/task-log-service.ts @@ -424,8 +424,8 @@ export class TaskLogService extends EventEmitter { for (const phase of phases) { if (logs.phases[phase]?.status === 'active') { - // Mark as failed since the phase was interrupted/stopped - logs.phases[phase].status = 'failed'; + // Mark as stopped since the task was stopped by the user + logs.phases[phase].status = 'stopped'; logs.phases[phase].completed_at = new Date().toISOString(); hasChanges = true; } diff --git a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx index 3481b263f0..f70f0344b1 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx @@ -210,6 +210,13 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, p Failed ); + case 'stopped': + return ( + + + Stopped + + ); default: return ( @@ -232,6 +239,7 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, p isInterrupted && 'border-warning/30 bg-warning/5', status === 'completed' && 'border-success/30 bg-success/5', status === 'failed' && 'border-destructive/30 bg-destructive/5', + status === 'stopped' && 'border-muted-foreground/30 bg-muted/5', status === 'pending' && 'border-border bg-secondary/30' )} > diff --git a/apps/frontend/src/shared/types/task.ts b/apps/frontend/src/shared/types/task.ts index 42c9e61a61..eb0c681c86 100644 --- a/apps/frontend/src/shared/types/task.ts +++ b/apps/frontend/src/shared/types/task.ts @@ -58,7 +58,7 @@ export interface QAIssue { // Task Log Types - for persistent, phase-based logging export type TaskLogPhase = 'planning' | 'coding' | 'validation'; -export type TaskLogPhaseStatus = 'pending' | 'active' | 'completed' | 'failed'; +export type TaskLogPhaseStatus = 'pending' | 'active' | 'completed' | 'failed' | 'stopped'; export type TaskLogEntryType = 'text' | 'tool_start' | 'tool_end' | 'phase_start' | 'phase_end' | 'error' | 'success' | 'info'; export interface TaskLogEntry { From 9c2fea0acd86846d9e935643d89c2b024133498a Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Sat, 27 Dec 2025 17:20:07 +0000 Subject: [PATCH 5/6] feat: add i18n support for task log status labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create taskLogs namespace with status translations (EN/FR) - Update TaskLogs.tsx to use translation keys instead of hardcoded strings - Register taskLogs namespace in i18n configuration - Fix CodeQL race condition warning in task-log-service.ts by removing redundant existsSync check Translation keys added: - status.pending - status.active (Running) - status.completed (Complete) - status.failed - status.stopped - status.interrupted đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/frontend/src/main/task-log-service.ts | 4 ++-- .../renderer/components/task-detail/TaskLogs.tsx | 14 ++++++++------ apps/frontend/src/shared/i18n/index.ts | 10 +++++++--- .../src/shared/i18n/locales/en/taskLogs.json | 10 ++++++++++ .../src/shared/i18n/locales/fr/taskLogs.json | 10 ++++++++++ 5 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 apps/frontend/src/shared/i18n/locales/en/taskLogs.json create mode 100644 apps/frontend/src/shared/i18n/locales/fr/taskLogs.json diff --git a/apps/frontend/src/main/task-log-service.ts b/apps/frontend/src/main/task-log-service.ts index f3f6d99d88..44cbd8a61c 100644 --- a/apps/frontend/src/main/task-log-service.ts +++ b/apps/frontend/src/main/task-log-service.ts @@ -413,9 +413,8 @@ export class TaskLogService extends EventEmitter { * Update a single log file to mark active phases as stopped */ private updateLogFilePhaseStatus(logFilePath: string): void { - if (!existsSync(logFilePath)) return; - try { + // Read file - if it doesn't exist, readFileSync will throw and we'll catch it const content = readFileSync(logFilePath, 'utf-8'); const logs = JSON.parse(content) as TaskLogs; @@ -438,6 +437,7 @@ export class TaskLogService extends EventEmitter { console.log(`[TaskLogService] Marked active phases as stopped in ${logFilePath}`); } } catch (error) { + // File doesn't exist, is corrupted, or write failed - log and continue console.error(`[TaskLogService] Failed to update log file ${logFilePath}:`, error); } } diff --git a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx index f70f0344b1..ce46c0dc12 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Terminal, Loader2, @@ -175,6 +176,7 @@ interface PhaseLogSectionProps { } function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, phaseConfig }: PhaseLogSectionProps) { + const { t } = useTranslation('taskLogs'); const Icon = PHASE_ICONS[phase]; const status = phaseLog?.status || 'pending'; const hasEntries = (phaseLog?.entries.length || 0) > 0; @@ -186,41 +188,41 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, p return ( - Interrupted + {t('status.interrupted')} ); } return ( - Running + {t('status.active')} ); case 'completed': return ( - Complete + {t('status.completed')} ); case 'failed': return ( - Failed + {t('status.failed')} ); case 'stopped': return ( - Stopped + {t('status.stopped')} ); default: return ( - Pending + {t('status.pending')} ); } diff --git a/apps/frontend/src/shared/i18n/index.ts b/apps/frontend/src/shared/i18n/index.ts index 407d8d4c75..a3a6283142 100644 --- a/apps/frontend/src/shared/i18n/index.ts +++ b/apps/frontend/src/shared/i18n/index.ts @@ -10,6 +10,7 @@ import enWelcome from './locales/en/welcome.json'; import enOnboarding from './locales/en/onboarding.json'; import enDialogs from './locales/en/dialogs.json'; import enTaskReview from './locales/en/taskReview.json'; +import enTaskLogs from './locales/en/taskLogs.json'; // Import French translation resources import frCommon from './locales/fr/common.json'; @@ -20,6 +21,7 @@ import frWelcome from './locales/fr/welcome.json'; import frOnboarding from './locales/fr/onboarding.json'; import frDialogs from './locales/fr/dialogs.json'; import frTaskReview from './locales/fr/taskReview.json'; +import frTaskLogs from './locales/fr/taskLogs.json'; export const defaultNS = 'common'; @@ -32,7 +34,8 @@ export const resources = { welcome: enWelcome, onboarding: enOnboarding, dialogs: enDialogs, - taskReview: enTaskReview + taskReview: enTaskReview, + taskLogs: enTaskLogs }, fr: { common: frCommon, @@ -42,7 +45,8 @@ export const resources = { welcome: frWelcome, onboarding: frOnboarding, dialogs: frDialogs, - taskReview: frTaskReview + taskReview: frTaskReview, + taskLogs: frTaskLogs } } as const; @@ -53,7 +57,7 @@ i18n lng: 'en', // Default language (will be overridden by settings) fallbackLng: 'en', defaultNS, - ns: ['common', 'navigation', 'settings', 'tasks', 'welcome', 'onboarding', 'dialogs', 'taskReview'], + ns: ['common', 'navigation', 'settings', 'tasks', 'welcome', 'onboarding', 'dialogs', 'taskReview', 'taskLogs'], interpolation: { escapeValue: false // React already escapes values }, diff --git a/apps/frontend/src/shared/i18n/locales/en/taskLogs.json b/apps/frontend/src/shared/i18n/locales/en/taskLogs.json new file mode 100644 index 0000000000..bd005772af --- /dev/null +++ b/apps/frontend/src/shared/i18n/locales/en/taskLogs.json @@ -0,0 +1,10 @@ +{ + "status": { + "pending": "Pending", + "active": "Running", + "completed": "Complete", + "failed": "Failed", + "stopped": "Stopped", + "interrupted": "Interrupted" + } +} diff --git a/apps/frontend/src/shared/i18n/locales/fr/taskLogs.json b/apps/frontend/src/shared/i18n/locales/fr/taskLogs.json new file mode 100644 index 0000000000..72dc9840b7 --- /dev/null +++ b/apps/frontend/src/shared/i18n/locales/fr/taskLogs.json @@ -0,0 +1,10 @@ +{ + "status": { + "pending": "En attente", + "active": "En cours", + "completed": "TerminĂ©", + "failed": "ÉchouĂ©", + "stopped": "ArrĂȘtĂ©", + "interrupted": "Interrompu" + } +} From 52d4c3663b574141d58190508e23fd1d5df1e4f5 Mon Sep 17 00:00:00 2001 From: Joshua Riley Date: Mon, 29 Dec 2025 10:28:32 +0000 Subject: [PATCH 6/6] fix(task-logs): add security hardening for file operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR review feedback with the following security improvements: 1. **Atomic file writes**: Use temp file + rename pattern to prevent file corruption during concurrent access. This ensures writes are atomic and matches the pattern used in the Python backend. 2. **Path traversal protection**: Add specId validation to prevent directory traversal attacks. Validates format (XXX-name) and rejects paths containing '..' or directory separators. Fixes the two remaining high-priority security issues from PR review. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/frontend/src/main/task-log-service.ts | 41 ++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/main/task-log-service.ts b/apps/frontend/src/main/task-log-service.ts index 44cbd8a61c..16bfedb377 100644 --- a/apps/frontend/src/main/task-log-service.ts +++ b/apps/frontend/src/main/task-log-service.ts @@ -1,5 +1,5 @@ import path from 'path'; -import { existsSync, readFileSync, writeFileSync, watchFile } from 'fs'; +import { existsSync, readFileSync, writeFileSync, watchFile, renameSync, unlinkSync } from 'fs'; import { EventEmitter } from 'events'; import type { TaskLogs, TaskLogPhase, TaskLogStreamChunk, TaskPhaseLog } from '../shared/types'; @@ -386,6 +386,12 @@ export class TaskLogService extends EventEmitter { * @param specsRelPath - Relative path to specs (e.g., ".auto-claude/specs") */ markActivePhasesStopped(specId: string, projectPath: string, specsRelPath: string): void { + // Validate specId to prevent path traversal attacks + if (!this.isValidSpecId(specId)) { + console.error(`[TaskLogService] Invalid specId format: ${specId}`); + return; + } + // Get watched paths for this spec const watchedInfo = this.watchedPaths.get(specId); @@ -411,6 +417,7 @@ export class TaskLogService extends EventEmitter { /** * Update a single log file to mark active phases as stopped + * Uses atomic write (temp file + rename) to prevent corruption */ private updateLogFilePhaseStatus(logFilePath: string): void { try { @@ -433,14 +440,42 @@ export class TaskLogService extends EventEmitter { // Only write if we made changes if (hasChanges) { logs.updated_at = new Date().toISOString(); - writeFileSync(logFilePath, JSON.stringify(logs, null, 2), 'utf-8'); - console.log(`[TaskLogService] Marked active phases as stopped in ${logFilePath}`); + + // Use atomic write: temp file + rename to prevent corruption + const tempFilePath = `${logFilePath}.tmp`; + try { + writeFileSync(tempFilePath, JSON.stringify(logs, null, 2), 'utf-8'); + renameSync(tempFilePath, logFilePath); + console.log(`[TaskLogService] Marked active phases as stopped in ${logFilePath}`); + } catch (writeError) { + // Clean up temp file if it exists + if (existsSync(tempFilePath)) { + unlinkSync(tempFilePath); + } + throw writeError; + } } } catch (error) { // File doesn't exist, is corrupted, or write failed - log and continue console.error(`[TaskLogService] Failed to update log file ${logFilePath}:`, error); } } + + /** + * Validate specId format to prevent path traversal attacks + * Valid format: 3 digits followed by dash and alphanumeric characters with dashes + * Example: "001-feature-name" or "123-bug-fix" + */ + private isValidSpecId(specId: string): boolean { + // Check for path traversal patterns + if (specId.includes('..') || specId.includes('/') || specId.includes('\\')) { + return false; + } + + // Validate format: XXX-name (e.g., 001-feature, 123-bug-fix) + const specIdPattern = /^[0-9]{3}-[a-z0-9-]+$/; + return specIdPattern.test(specId); + } } // Singleton instance