Skip to content
Open
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
15 changes: 15 additions & 0 deletions apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -185,6 +186,20 @@ export function registerTaskExecutionHandlers(
agentManager.killTask(taskId);
fileWatcher.unwatch(taskId);

// 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 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();
if (mainWindow) {
mainWindow.webContents.send(
Expand Down
102 changes: 101 additions & 1 deletion apps/frontend/src/main/task-log-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'path';
import { existsSync, readFileSync, 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';

Expand Down Expand Up @@ -376,6 +376,106 @@ export class TaskLogService extends EventEmitter {
const logFile = path.join(specDir, 'task_logs.json');
return existsSync(logFile);
}

Copy link

Choose a reason for hiding this comment

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

Missing phase_end event emission for stopped status

The emitNewEntries function emits phase_end stream-chunk events when phases transition to completed or failed, but the newly added stopped status is not included in this condition. When a phase transitions from active to stopped, listeners expecting phase_end events won't receive them, creating inconsistent event behavior. The condition at line 319 needs to include || currPhase.status === 'stopped' to maintain consistency across all terminal phase statuses.

Fix in Cursor Fix in Web

/**
* 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 {
// 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);

// 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);

Comment on lines +388 to +401
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the full context of the markActivePhasesStopped method and find all callers

echo "=== Looking for markActivePhasesStopped method definition and calls ==="
rg -n 'markActivePhasesStopped' --type=ts

echo -e "\n=== Checking execution-handlers.ts for task stop handlers ==="
rg -n -A 15 -B 3 'TASK_STOP' apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1663


🏁 Script executed:

#!/bin/bash
# Verify the sources of the parameters passed to markActivePhasesStopped

echo "=== Finding findTaskAndProject function definition ==="
rg -n -A 15 'function findTaskAndProject|const findTaskAndProject' --type=ts

echo -e "\n=== Finding getSpecsDir function definition ==="
rg -n -A 10 'function getSpecsDir|const getSpecsDir' --type=ts

echo -e "\n=== Checking if project.path and task.specId are user-controllable ==="
rg -n -B 5 -A 3 'interface.*Project|type.*Project' apps/frontend/src/main/ --type=ts | head -50

Repository: AndyMik90/Auto-Claude

Length of output: 6557


🏁 Script executed:

#!/bin/bash
# Check where projects are created/added and how paths are validated

echo "=== Looking for project creation/import logic ==="
rg -n 'addProject|createProject|importProject|projects\.push' apps/frontend/src/main/ --type=ts | head -20

echo -e "\n=== Checking ProjectStore addProject method ==="
rg -n -A 20 'addProject.*{' apps/frontend/src/main/project-store.ts

echo -e "\n=== Checking if project paths are validated ==="
rg -n -B 5 -A 10 'path.isAbsolute|path.resolve|normalize' apps/frontend/src/main/project-store.ts | head -40

Repository: AndyMik90/Auto-Claude

Length of output: 2562


🏁 Script executed:

#!/bin/bash
# Check the addProject method properly

echo "=== Looking for addProject method implementation ==="
rg -n -A 30 'addProject\(projectPath' apps/frontend/src/main/project-store.ts

echo -e "\n=== Check where project paths come from in IPC handlers ==="
rg -n -B 10 'projectStore.addProject' apps/frontend/src/main/ipc-handlers/project-handlers.ts

echo -e "\n=== Check if paths are validated before being used ==="
rg -n 'existsSync|stat|isDirectory' apps/frontend/src/main/ipc-handlers/project-handlers.ts | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 3060


🏁 Script executed:

#!/bin/bash
# Check how the project store is persisted and loaded

echo "=== Check projectStore save/load mechanism ==="
rg -n -A 15 'save\(' apps/frontend/src/main/project-store.ts | head -30

echo -e "\n=== Check how projects are loaded from storage ==="
rg -n -A 15 'load\(' apps/frontend/src/main/project-store.ts | head -40

echo -e "\n=== Check if there's any editing of project paths after creation ==="
rg -n 'path.*=' apps/frontend/src/main/project-store.ts | grep -v 'projectPath\|basePath\|projectName\|autoBuildPath'

Repository: AndyMik90/Auto-Claude

Length of output: 2102


🏁 Script executed:

#!/bin/bash
# Final verification: check if projectPath or specsRelPath are ever passed through IPC

echo "=== Check all IPC calls to markActivePhasesStopped ==="
rg -n -B 20 'markActivePhasesStopped' apps/frontend/src/main/ --type=ts | grep -A 20 -B 20 'ipcMain'

echo -e "\n=== Verify projectPath source in execution-handlers ==="
rg -n -B 30 'taskLogService.markActivePhasesStopped' apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts | tail -50

echo -e "\n=== Check if any IPC handler takes path parameters ==="
rg -n 'ipcMain\.(on|handle).*projectPath|ipcMain\.(on|handle).*specsRelPath' apps/frontend/src/main/ --type=ts

Repository: AndyMik90/Auto-Claude

Length of output: 4231


🏁 Script executed:

#!/bin/bash
# Check if there are other entry points that could pass arbitrary projectPath/specsRelPath

echo "=== Check all methods that call updateLogFilePhaseStatus ==="
rg -n 'updateLogFilePhaseStatus' apps/frontend/src/main/ --type=ts

echo -e "\n=== Verify getSpecsDir implementation ==="
cat apps/frontend/src/shared/constants/config.ts | grep -A 5 'function getSpecsDir'

echo -e "\n=== Check if there are any other paths to modify project.path or autoBuildPath ==="
rg -n 'project\.path\s*=' apps/frontend/src/main/ --type=ts

Repository: AndyMik90/Auto-Claude

Length of output: 774


🏁 Script executed:

#!/bin/bash
# Final verification: trace complete parameter flow

echo "=== Verify complete parameter source chain ==="
echo "1. TASK_STOP handler receives taskId (UUID)"
echo "2. findTaskAndProject(taskId) is called"
echo "3. Let's check what projectStore actually returns"

rg -n -A 20 'getProjects\(\)|getTasks\(' apps/frontend/src/main/project-store.ts

echo -e "\n=== Check if projectStore data can be modified via IPC ==="
rg -n 'ipcMain.*projectStore' apps/frontend/src/main/ --type=ts

echo -e "\n=== Confirm: are project paths validated when retrieved from store ==="
rg -n -B 5 -A 5 'projects\s*=' apps/frontend/src/main/project-store.ts | head -30

Repository: AndyMik90/Auto-Claude

Length of output: 2403


🏁 Script executed:

#!/bin/bash
# One final check: see if specId itself is properly validated

echo "=== Check isValidSpecId implementation ==="
rg -n -A 10 'isValidSpecId' apps/frontend/src/main/task-log-service.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1074


Consider adding path validation as defense-in-depth, though parameters come from trusted application storage.

While projectPath and specsRelPath are not validated for path traversal, these parameters originate from the application's own persistent storage rather than untrusted user input. They are retrieved from projectStore in the TASK_STOP IPC handler and come from project paths that were already validated with existsSync() during project creation.

However, adding validation for these parameters would still be reasonable as a defense-in-depth measure:

  • Check that normalized paths don't contain .. after being loaded from storage
  • Ensure paths remain within expected project boundaries

The approach in the proposed fix is sound, but the severity is lower than initially indicated since an attacker would need to compromise the application's persistent store file itself, not exploit untrusted IPC input.

🤖 Prompt for AI Agents
In apps/frontend/src/main/task-log-service.ts around lines 388 to 401, add
defense-in-depth validation for projectPath and specsRelPath before using them
to build file system paths: normalize and resolve each input (path.normalize +
path.resolve), reject or bail if the normalized value contains '..' path
segments or if the resolved path is not contained within the expected project
root (compare resolvedSpecPath.startsWith(resolvedProjectRoot + path.sep) or
equivalent), and log an error before returning; keep the existing isValidSpecId
check and use the validated/normalized paths when constructing mainSpecDir and
worktreeSpecDir.

// 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
* Uses atomic write (temp file + rename) to prevent corruption
*/
private updateLogFilePhaseStatus(logFilePath: string): void {
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;

let hasChanges = false;
const phases: TaskLogPhase[] = ['planning', 'coding', 'validation'];

for (const phase of phases) {
if (logs.phases[phase]?.status === 'active') {
// 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;
}
}

// Only write if we made changes
if (hasChanges) {
logs.updated_at = new Date().toISOString();

// 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);
Copy link

Choose a reason for hiding this comment

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

SpecId validation rejects specs numbered 1000 or higher

The isValidSpecId regex pattern ^[0-9]{3}-[a-z0-9-]+$ requires exactly 3 digits at the start. However, spec numbers are generated using padStart(3, '0') which only pads smaller numbers but doesn't truncate larger ones. When a project has 1000+ specs, specIds like 1000-feature-name would fail validation, causing markActivePhasesStopped to silently return without updating the log files. The regex should use {3,} instead of {3} to allow 3 or more digits.

Fix in Cursor Fix in Web

}
}

// Singleton instance
Expand Down
20 changes: 15 additions & 5 deletions apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Terminal,
Loader2,
Expand Down Expand Up @@ -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;
Expand All @@ -186,34 +188,41 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, p
return (
<Badge variant="outline" className="text-xs bg-warning/10 text-warning border-warning/30 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
Interrupted
{t('status.interrupted')}
</Badge>
);
}
return (
<Badge variant="outline" className="text-xs bg-info/10 text-info border-info/30 flex items-center gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
Running
{t('status.active')}
</Badge>
);
case 'completed':
return (
<Badge variant="outline" className="text-xs bg-success/10 text-success border-success/30 flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" />
Complete
{t('status.completed')}
</Badge>
);
case 'failed':
return (
<Badge variant="outline" className="text-xs bg-destructive/10 text-destructive border-destructive/30 flex items-center gap-1">
<XCircle className="h-3 w-3" />
Failed
{t('status.failed')}
</Badge>
);
case 'stopped':
return (
<Badge variant="outline" className="text-xs bg-muted/10 text-muted-foreground border-muted-foreground/30 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{t('status.stopped')}
</Badge>
);
default:
return (
<Badge variant="secondary" className="text-xs text-muted-foreground">
Pending
{t('status.pending')}
</Badge>
);
}
Expand All @@ -232,6 +241,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'
)}
>
Expand Down
10 changes: 7 additions & 3 deletions apps/frontend/src/shared/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -32,7 +34,8 @@ export const resources = {
welcome: enWelcome,
onboarding: enOnboarding,
dialogs: enDialogs,
taskReview: enTaskReview
taskReview: enTaskReview,
taskLogs: enTaskLogs
},
fr: {
common: frCommon,
Expand All @@ -42,7 +45,8 @@ export const resources = {
welcome: frWelcome,
onboarding: frOnboarding,
dialogs: frDialogs,
taskReview: frTaskReview
taskReview: frTaskReview,
taskLogs: frTaskLogs
}
} as const;

Expand All @@ -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
},
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/shared/i18n/locales/en/taskLogs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"status": {
"pending": "Pending",
"active": "Running",
"completed": "Complete",
"failed": "Failed",
"stopped": "Stopped",
"interrupted": "Interrupted"
}
}
10 changes: 10 additions & 0 deletions apps/frontend/src/shared/i18n/locales/fr/taskLogs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"status": {
"pending": "En attente",
"active": "En cours",
"completed": "Terminé",
"failed": "Échoué",
"stopped": "Arrêté",
"interrupted": "Interrompu"
}
}
2 changes: 1 addition & 1 deletion apps/frontend/src/shared/types/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading