Skip to content
Merged
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,4 @@ OPUS_ANALYSIS_AND_IDEAS.md
.security-key
/shared_docs
logs/security/
Agents.md
packages/
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The removal of packages/ from the .gitignore file is a significant change that could have unintended consequences, such as committing node modules or other dependencies into the repository. This change seems unrelated to the PR's goal of adding an expand button. Could you please clarify the reason for this change or revert it if it was unintentional?

Agents.md
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

Script executed:

# Check git history for .gitignore to see what happened with Agents.md
git log --oneline -20 -- .gitignore

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0

stdout:

1205444 fix: remove unrelated changes from branch (qa-requested)

Script executed:

# Show the diff for .gitignore to see the exact change
git show HEAD:.gitignore | grep -n -A 2 -B 2 "Agents.md"

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0

stdout:

174-/shared_docs
175-logs/security/
176:Agents.md

Script executed:

# Search for any references to Agents.md in the codebase
rg "Agents\.md" --type-list

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 2

stderr:

error: The argument '<PATTERN>' cannot be used with '--type-list'

USAGE:
    
    rg [OPTIONS] PATTERN [PATH ...]
    rg [OPTIONS] -e PATTERN ... [PATH ...]
    rg [OPTIONS] -f PATTERNFILE ... [PATH ...]
    rg [OPTIONS] --files [PATH ...]
    rg [OPTIONS] --type-list
    command | rg [OPTIONS] PATTERN
    rg [OPTIONS] --help
    rg [OPTIONS] --version

For more information try --help


Script executed:

# Check if Agents.md exists in the repository
find . -name "Agents.md" -type f 2>/dev/null

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0


Script executed:

# Search for any references to "Agents" in the codebase
rg "Agents" --type-list

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 2

stderr:

error: The argument '<PATTERN>' cannot be used with '--type-list'

USAGE:
    
    rg [OPTIONS] PATTERN [PATH ...]
    rg [OPTIONS] -e PATTERN ... [PATH ...]
    rg [OPTIONS] -f PATTERNFILE ... [PATH ...]
    rg [OPTIONS] --files [PATH ...]
    rg [OPTIONS] --type-list
    command | rg [OPTIONS] PATTERN
    rg [OPTIONS] --help
    rg [OPTIONS] --version

For more information try --help


Script executed:

# Correct search for Agents references
rg "Agents"

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0

stdout:

README.md:5. **Watch it work** - Agents plan, code, and validate autonomously
README.md:| **Memory Layer** | Agents retain insights across sessions for smarter builds |
README.md:![Agent Terminals](.github/assets/Auto-Claude-Agents-terminals.png)
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:  const [expandedAgents, setExpandedAgents] = useState<Set<string>>(new Set());
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:    setExpandedAgents(prev => {
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:                expandedAgents={expandedAgents}
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:  expandedAgents: Set<string>;
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isStreaming = false, expandedAgents, onToggleAgent }: PhaseLogSectionProps) {
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:              expandedAgents={expandedAgents}
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:  expandedAgents: Set<string>;
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:function GroupedLogEntries({ entries, phase, expandedAgents, onToggleAgent }: GroupedLogEntriesProps) {
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:          isExpanded={expandedAgents.has(`${phase}-orchestrator-activity`)}
apps/frontend/src/renderer/components/github-prs/components/PRLogs.tsx:          isExpanded={expandedAgents.has(`${phase}-${group.agentName}`)}
apps/backend/security/git_validators.py:    Agents should not set user.name, user.email, etc. as this:
apps/backend/runners/github/services/parallel_followup_reviewer.py:    Specialist Agents:
apps/backend/runners/github/services/parallel_followup_reviewer.py:                f"[ParallelFollowup] Session complete. Agents invoked: {final_agents}"
apps/backend/runners/github/services/parallel_followup_reviewer.py:                f"[ParallelFollowup] Complete. Agents invoked: {final_agents}",
apps/backend/runners/github/services/parallel_followup_reviewer.py:Agents invoked: {agents_str}
apps/backend/runners/github/services/parallel_orchestrator_reviewer.py:                f"[ParallelOrchestrator] Session complete. Agents invoked: {final_agents}"
apps/backend/runners/github/services/parallel_orchestrator_reviewer.py:                f"[ParallelOrchestrator] Complete. Agents invoked: {final_agents}",
apps/backend/runners/github/services/parallel_orchestrator_reviewer.py:        # Agents used
apps/backend/runners/github/services/parallel_orchestrator_reviewer.py:            lines.append(f"**Specialist Agents Invoked:** {', '.join(agents_invoked)}")
apps/backend/runners/github/services/sdk_utils.py:        self._write(f"Agents invoked: {summary.get('agents_invoked', [])}")
apps/backend/prompts/github/pr_followup_orchestrator.md:## Available Specialist Agents
apps/backend/prompts/github/pr_followup_orchestrator.md:### Phase 2: Delegate to Agents (USE TASK TOOL)
apps/backend/prompts/github/pr_parallel_orchestrator.md:## Available Specialist Agents
apps/backend/prompts/github/pr_parallel_orchestrator.md:## CRITICAL: How to Invoke Specialist Agents
apps/backend/prompts_pkg/prompt_generator.py:    # Agents no longer need to call Linear MCP tools directly
apps/backend/agents/tools_pkg/__init__.py:Custom MCP Tools for Auto-Claude Agents
apps/backend/agents/__init__.py:Agents Module
apps/backend/agents/README.md:# Agents Module

Script executed:

# Check git diff to see the actual change made to .gitignore in this PR
git diff HEAD~1 .gitignore

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 128

stderr:

fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

Script executed:

# Check the git history more carefully - see commits affecting .gitignore
git log --oneline -- .gitignore | head -10

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0

stdout:

1205444 fix: remove unrelated changes from branch (qa-requested)

Script executed:

# Get more context about the current .gitignore - check if Agents.md is a new addition
git diff --no-index /dev/null .gitignore 2>&1 | grep -A 5 -B 5 "Agents.md" || cat .gitignore | tail -20

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0

stdout:

+
+# Auto Claude generated files
+.security-key
+/shared_docs
+logs/security/
+Agents.md
\ No newline at end of file

Script executed:

# Check what files are in the PR - look at all changes in this commit
git diff --name-only HEAD

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0


Script executed:

# Check the commit message for more context
git log -1 --format=%B

Repository: AndyMik90/Auto-Claude


Repository: AndyMik90/Auto-Claude
Exit code: 0

stdout:

fix: remove unrelated changes from branch (qa-requested)

Reset files that were not related to the expand button feature back to
their develop branch state:
- .gitignore
- apps/backend/agents/ (base.py, coder.py, planner.py, session.py)
- apps/backend/core/ (client.py, simple_client.py)
- apps/frontend/src/renderer/App.tsx
- apps/frontend/src/renderer/components/AuthStatusIndicator.tsx
- apps/frontend/src/renderer/components/KanbanBoard.tsx
- apps/frontend/src/renderer/stores/task-store.ts
- apps/frontend/src/shared/i18n/locales/*/common.json
- tests/test_auth.py
- tests/test_issue_884_plan_schema.py

The expand button feature implementation remains intact.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>


Contradiction: commit claims to reset .gitignore but actually adds new entries.

The commit message states that .gitignore was reset to remove unrelated changes from the branch, yet the diff shows new entries being added (Agents.md, .security-key, /shared_docs, logs/security/) under a "# Auto Claude generated files" section. Since Agents.md does not exist in the repository and is unrelated to the expand button feature, clarify whether these additions are intentional or should be reverted to match the commit's stated objective of removing unrelated changes.

🤖 Prompt for AI Agents
In @.gitignore at line 176, The .gitignore shows new entries under the "# Auto
Claude generated files" section (e.g., Agents.md, .security-key, /shared_docs,
logs/security/) that contradict the commit message claiming a reset; either
remove these added lines to truly revert unrelated changes or explicitly
document and justify them in the commit message. Locate the "# Auto Claude
generated files" block and delete the specific entries (Agents.md,
.security-key, /shared_docs, logs/security/) if they were unintended, or update
the commit message to explain why these ignores are required and ensure any
referenced files exist or are intentionally omitted.

7 changes: 0 additions & 7 deletions apps/backend/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,3 @@
# Configuration constants
AUTO_CONTINUE_DELAY_SECONDS = 3
HUMAN_INTERVENTION_FILE = "PAUSE"

# Retry configuration for 400 tool concurrency errors
MAX_CONCURRENCY_RETRIES = 5 # Maximum number of retries for tool concurrency errors
INITIAL_RETRY_DELAY_SECONDS = (
2 # Initial retry delay (doubles each retry: 2s, 4s, 8s, 16s, 32s)
)
MAX_RETRY_DELAY_SECONDS = 32 # Cap retry delay at 32 seconds
3 changes: 1 addition & 2 deletions apps/frontend/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { GitHubSetupModal } from './components/GitHubSetupModal';
import { useProjectStore, loadProjects, addProject, initializeProject, removeProject } from './stores/project-store';
import { useTaskStore, loadTasks } from './stores/task-store';
import { useSettingsStore, loadSettings, loadProfiles, saveSettings } from './stores/settings-store';
import { useClaudeProfileStore, loadClaudeProfiles } from './stores/claude-profile-store';
import { useClaudeProfileStore } from './stores/claude-profile-store';
import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store';
import { initializeGitHubListeners } from './stores/github';
import { initDownloadProgressListener } from './stores/download-store';
Comment on lines 57 to 63

This comment was marked as outdated.

Expand Down Expand Up @@ -184,7 +184,6 @@ export function App() {
loadProjects();
loadSettings();
loadProfiles();
loadClaudeProfiles();
// Initialize global GitHub listeners (PR reviews, etc.) so they persist across navigation
initializeGitHubListeners();
// Initialize global download progress listener for Ollama model downloads
Expand Down
57 changes: 5 additions & 52 deletions apps/frontend/src/renderer/components/AuthStatusIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
} from './ui/tooltip';
import { useTranslation } from 'react-i18next';
import { useSettingsStore } from '../stores/settings-store';
import { useClaudeProfileStore } from '../stores/claude-profile-store';
import { detectProvider, getProviderLabel, getProviderBadgeColor, type ApiProvider } from '../../shared/utils/provider-detection';
import { formatTimeRemaining, localizeUsageWindowLabel, hasHardcodedText } from '../../shared/utils/format-time';
import type { ClaudeUsageSnapshot } from '../../shared/types/agent';
Expand All @@ -50,13 +49,8 @@ const OAUTH_FALLBACK = {
} as const;

export function AuthStatusIndicator() {
// Subscribe to profile state from settings store (API profiles)
// Subscribe to profile state from settings store
const { profiles, activeProfileId } = useSettingsStore();

// Subscribe to Claude OAuth profile state
const claudeProfiles = useClaudeProfileStore((state) => state.profiles);
const activeClaudeProfileId = useClaudeProfileStore((state) => state.activeProfileId);

const { t } = useTranslation(['common']);

// Track usage data for warning badge
Expand Down Expand Up @@ -108,7 +102,6 @@ export function AuthStatusIndicator() {

// Compute auth status and provider detection using useMemo to avoid unnecessary re-renders
const authStatus = useMemo(() => {
// First check if user is using API profile auth (has active API profile)
if (activeProfileId) {
const activeProfile = profiles.find(p => p.id === activeProfileId);
if (activeProfile) {
Expand All @@ -126,36 +119,12 @@ export function AuthStatusIndicator() {
badgeColor: getProviderBadgeColor(provider)
};
}
// Profile ID set but profile not found - fallback to OAuth
return OAUTH_FALLBACK;
}

// No active API profile - check Claude OAuth profiles directly
if (activeClaudeProfileId && claudeProfiles.length > 0) {
const activeClaudeProfile = claudeProfiles.find(p => p.id === activeClaudeProfileId);
if (activeClaudeProfile) {
return {
type: 'oauth' as const,
name: activeClaudeProfile.email || activeClaudeProfile.name,
provider: 'anthropic' as const,
providerLabel: 'Anthropic',
badgeColor: 'bg-orange-500/10 text-orange-500 border-orange-500/20 hover:bg-orange-500/15'
};
}
}

// Fallback to usage data if Claude profiles aren't loaded yet
if (usage && (usage.profileName || usage.profileEmail)) {
return {
type: 'oauth' as const,
name: usage.profileEmail || usage.profileName,
provider: 'anthropic' as const,
providerLabel: 'Anthropic',
badgeColor: 'bg-orange-500/10 text-orange-500 border-orange-500/20 hover:bg-orange-500/15'
};
}

// No auth info available - fallback to generic OAuth
// No active profile - using OAuth
return OAUTH_FALLBACK;
}, [activeProfileId, profiles, activeClaudeProfileId, claudeProfiles, usage]);
}, [activeProfileId, profiles]);

// Helper function to truncate ID for display
const truncateId = (id: string): string => {
Expand Down Expand Up @@ -305,22 +274,6 @@ export function AuthStatusIndicator() {
</div>
</>
)}

{/* Account details for OAuth profiles */}
{isOAuth && authStatus.name && authStatus.name !== 'OAuth' && (
<>
<div className="pt-2 border-t space-y-2">
{/* Account name/email with icon */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Lock className="h-3 w-3" />
<span className="text-[10px]">{t('common:usage.account')}</span>
</div>
<span className="font-medium text-[10px]">{authStatus.name}</span>
</div>
</div>
</>
)}
</div>
</TooltipContent>
</Tooltip>
Expand Down
120 changes: 20 additions & 100 deletions apps/frontend/src/renderer/components/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { TaskCard } from './TaskCard';
import { SortableTaskCard } from './SortableTaskCard';
import { QueueSettingsModal } from './QueueSettingsModal';
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
import { debugLog } from '../../shared/utils/debug-logger';
import { cn } from '../lib/utils';
import { persistTaskStatus, forceCompleteTask, archiveTasks, deleteTasks, useTaskStore } from '../stores/task-store';
import { updateProjectSettings, useProjectStore } from '../stores/project-store';
Expand Down Expand Up @@ -1068,74 +1067,29 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
isProcessingQueueRef.current = true;

try {
// Track tasks we've already processed in this call to prevent duplicates
// This is critical because store updates happen synchronously but we need to ensure
// we never process the same task twice, even if there are timing issues
const processedTaskIds = new Set<string>();
// Track tasks we've already attempted to promote (to avoid infinite retries)
const attemptedTaskIds = new Set<string>();
let consecutiveFailures = 0;
const MAX_CONSECUTIVE_FAILURES = 10; // Safety limit to prevent infinite loop

// Track promotions in this call to enforce max parallel tasks limit
let promotedInThisCall = 0;

// Log initial state
const initialTasks = useTaskStore.getState().tasks;
const initialInProgress = initialTasks.filter((t) => t.status === 'in_progress' && !t.metadata?.archivedAt);
const initialQueued = initialTasks.filter((t) => t.status === 'queue' && !t.metadata?.archivedAt);
debugLog(`[Queue] === PROCESS QUEUE START ===`, {
maxParallelTasks,
initialInProgressCount: initialInProgress.length,
initialInProgressIds: initialInProgress.map(t => t.id),
initialQueuedCount: initialQueued.length,
initialQueuedIds: initialQueued.map(t => t.id),
projectId
});

// Loop until capacity is full or queue is empty
let iteration = 0;
while (true) {
iteration++;
// Calculate total in-progress count: tasks that were already in progress + tasks promoted in this call
const totalInProgressCount = initialInProgress.length + promotedInThisCall;

debugLog(`[Queue] --- Iteration ${iteration} ---`, {
initialInProgressCount: initialInProgress.length,
promotedInThisCall,
totalInProgressCount,
capacityCheck: totalInProgressCount >= maxParallelTasks,
processedCount: processedTaskIds.size
});

// Stop if no capacity (initial in-progress + promoted in this call)
if (totalInProgressCount >= maxParallelTasks) {
debugLog(`[Queue] Capacity reached (${totalInProgressCount}/${maxParallelTasks}), stopping queue processing`);
break;
}

// Get CURRENT state from store to find queued tasks
const latestTasks = useTaskStore.getState().tasks;
const latestInProgress = latestTasks.filter((t) => t.status === 'in_progress' && !t.metadata?.archivedAt);
const queuedTasks = latestTasks.filter((t) =>
t.status === 'queue' && !t.metadata?.archivedAt && !processedTaskIds.has(t.id)
// Get CURRENT state from store to ensure accuracy
const currentTasks = useTaskStore.getState().tasks;
const inProgressCount = currentTasks.filter((t) =>
t.status === 'in_progress' && !t.metadata?.archivedAt
).length;
const queuedTasks = currentTasks.filter((t) =>
t.status === 'queue' && !t.metadata?.archivedAt && !attemptedTaskIds.has(t.id)
);

debugLog(`[Queue] Current store state:`, {
totalTasks: latestTasks.length,
inProgressCount: latestInProgress.length,
inProgressIds: latestInProgress.map(t => t.id),
queuedCount: queuedTasks.length,
queuedIds: queuedTasks.map(t => t.id),
processedIds: Array.from(processedTaskIds)
});

// Stop if no queued tasks or too many consecutive failures
if (queuedTasks.length === 0) {
debugLog('[Queue] No more queued tasks to process');
// Stop if no capacity, no queued tasks, or too many consecutive failures
if (inProgressCount >= maxParallelTasks || queuedTasks.length === 0) {
break;
}

if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
debugLog(`[Queue] Stopping queue processing after ${MAX_CONSECUTIVE_FAILURES} consecutive failures`);
console.warn(`[Queue] Stopping queue processing after ${MAX_CONSECUTIVE_FAILURES} consecutive failures`);
break;
}

Expand All @@ -1146,62 +1100,28 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
return dateA - dateB; // Ascending order (oldest first)
})[0];

debugLog(`[Queue] Selected task for promotion:`, {
id: nextTask.id,
currentStatus: nextTask.status,
title: nextTask.title?.substring(0, 50)
});

// Mark task as processed BEFORE attempting promotion to prevent duplicates
processedTaskIds.add(nextTask.id);

debugLog(`[Queue] Promoting task ${nextTask.id} (${promotedInThisCall + 1}/${maxParallelTasks})`);
console.log(`[Queue] Auto-promoting task ${nextTask.id} from Queue to In Progress (${inProgressCount + 1}/${maxParallelTasks})`);
const result = await persistTaskStatus(nextTask.id, 'in_progress');

// Check store state after promotion
const afterPromoteTasks = useTaskStore.getState().tasks;
const afterPromoteInProgress = afterPromoteTasks.filter((t) => t.status === 'in_progress' && !t.metadata?.archivedAt);
const afterPromoteQueued = afterPromoteTasks.filter((t) => t.status === 'queue' && !t.metadata?.archivedAt);

debugLog(`[Queue] After promotion attempt:`, {
resultSuccess: result.success,
promotedInThisCall,
inProgressCount: afterPromoteInProgress.length,
inProgressIds: afterPromoteInProgress.map(t => t.id),
queuedCount: afterPromoteQueued.length,
queuedIds: afterPromoteQueued.map(t => t.id)
});

if (result.success) {
// Increment our local promotion counter
promotedInThisCall++;
// Reset consecutive failures on success
consecutiveFailures = 0;
} else {
// If promotion failed, log error and continue to next task
// If promotion failed, log error, mark as attempted, and skip to next task
console.error(`[Queue] Failed to promote task ${nextTask.id} to In Progress:`, result.error);
attemptedTaskIds.add(nextTask.id);
consecutiveFailures++;
}
}

// Log summary
debugLog(`[Queue] === PROCESS QUEUE COMPLETE ===`, {
totalIterations: iteration,
tasksProcessed: processedTaskIds.size,
tasksPromoted: promotedInThisCall,
processedIds: Array.from(processedTaskIds)
});

// Trigger UI refresh if tasks were promoted to ensure UI reflects all changes
// This handles the case where store updates are batched/delayed via IPC events
if (promotedInThisCall > 0 && onRefresh) {
debugLog('[Queue] Triggering UI refresh after queue promotion');
onRefresh();
// Log if we had failed tasks
if (attemptedTaskIds.size > 0) {
console.warn(`[Queue] Skipped ${attemptedTaskIds.size} task(s) that failed to promote`);
}
} finally {
isProcessingQueueRef.current = false;
}
}, [maxParallelTasks, projectId, onRefresh]);
}, [maxParallelTasks]);

// Register task status change listener for queue auto-promotion
// This ensures processQueue() is called whenever a task leaves in_progress
Expand All @@ -1210,7 +1130,7 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
(taskId, oldStatus, newStatus) => {
// When a task leaves in_progress (e.g., goes to human_review), process the queue
if (oldStatus === 'in_progress' && newStatus !== 'in_progress') {
debugLog(`[Queue] Task ${taskId} left in_progress, processing queue to fill slot`);
console.log(`[Queue] Task ${taskId} left in_progress, processing queue to fill slot`);
processQueue();
}
}
Expand Down
Loading
Loading