feat: introduce task archiving and Graphiti MCP server integration

- Added functionality to archive and unarchive tasks, allowing users to manage task visibility effectively.
- Implemented Graphiti MCP server integration, enabling agents to access and manipulate knowledge graphs directly.
- Enhanced project settings to include options for enabling the Graphiti MCP server and configuring its URL.
- Updated the Kanban board to filter and display archived tasks, improving task management.
- Introduced a changelog version suggestion feature based on task changes, streamlining release note generation.

These updates enhance task management capabilities and provide a more robust integration with knowledge graph functionalities.
This commit is contained in:
AndyMik90
2025-12-13 15:23:53 +01:00
parent a07ce185be
commit b0b5d71076
15 changed files with 1388 additions and 405 deletions
+14
View File
@@ -0,0 +1,14 @@
## What's New in v1.1.0
### New Features
- **Follow-up Tasks**: Continue working on completed specs by adding new tasks to existing implementations. The system automatically re-enters planning mode and integrates with your existing documentation and context.
- **Screenshot Support for Feedback**: Attach screenshots to your change requests when reviewing tasks, providing visual context for your feedback alongside text comments.
- **Unified Task Editing**: The Edit Task dialog now includes all the same options as the New Task dialog—classification metadata, image attachments, and review settings—giving you full control when modifying tasks.
### Improvements
- **Enhanced Kanban Board**: Improved visual design and interaction patterns for task cards, making it easier to scan status, understand progress, and work with tasks efficiently.
- **Screenshot Handling**: Paste screenshots directly into task descriptions using Ctrl+V (Cmd+V on Mac) for faster documentation.
- **Draft Auto-Save**: Task creation state is now automatically saved when you navigate away, preventing accidental loss of work-in-progress.
### Bug Fixes
- Fixed task editing to support the same comprehensive options available in new task creation
+43 -11
View File
@@ -3,6 +3,7 @@ import { EventEmitter } from 'events';
import path from 'path';
import { existsSync, readFileSync } from 'fs';
import { app } from 'electron';
import { projectStore } from './project-store';
interface AgentProcess {
taskId: string;
@@ -79,6 +80,27 @@ export class AgentManager extends EventEmitter {
return null;
}
/**
* Get project-specific environment variables based on project settings
*/
private getProjectEnvVars(projectPath: string): Record<string, string> {
const env: Record<string, string> = {};
// Find project by path
const projects = projectStore.getProjects();
const project = projects.find((p) => p.path === projectPath);
if (project?.settings) {
// Graphiti MCP integration
if (project.settings.graphitiMcpEnabled) {
const graphitiUrl = project.settings.graphitiMcpUrl || 'http://localhost:8000/mcp/';
env['GRAPHITI_MCP_URL'] = graphitiUrl;
}
}
return env;
}
/**
* Load environment variables from auto-claude .env file
*/
@@ -154,8 +176,10 @@ export class AgentManager extends EventEmitter {
return;
}
// Load environment variables from auto-claude .env file
// Load environment variables from auto-claude .env file and project settings
const autoBuildEnv = this.loadAutoBuildEnv();
const projectEnv = this.getProjectEnvVars(projectPath);
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
// spec_runner.py will auto-start run.py after spec creation completes
const args = [specRunnerPath, '--task', taskDescription, '--project-dir', projectPath];
@@ -181,7 +205,7 @@ export class AgentManager extends EventEmitter {
// Note: This is spec-creation but it chains to task-execution via run.py
// So we treat the whole thing as task-execution for status purposes
this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'task-execution');
this.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'task-execution');
}
/**
@@ -212,8 +236,10 @@ export class AgentManager extends EventEmitter {
return;
}
// Load environment variables from auto-claude .env file
// Load environment variables from auto-claude .env file and project settings
const autoBuildEnv = this.loadAutoBuildEnv();
const projectEnv = this.getProjectEnvVars(projectPath);
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
const args = [runPath, '--spec', specId, '--project-dir', projectPath];
@@ -235,7 +261,7 @@ export class AgentManager extends EventEmitter {
}
console.log('[AgentManager] Spawning process with args:', args);
this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'task-execution');
this.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'task-execution');
}
/**
@@ -262,8 +288,10 @@ export class AgentManager extends EventEmitter {
return;
}
// Load environment variables from auto-claude .env file
// Load environment variables from auto-claude .env file and project settings
const autoBuildEnv = this.loadAutoBuildEnv();
const projectEnv = this.getProjectEnvVars(projectPath);
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
const args = [runPath, '--spec', specId, '--project-dir', projectPath, '--qa'];
@@ -272,7 +300,7 @@ export class AgentManager extends EventEmitter {
args.push('--dev');
}
this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'qa-process');
this.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'qa-process');
}
/**
@@ -386,14 +414,16 @@ export class AgentManager extends EventEmitter {
const autoBuildSource = this.getAutoBuildSourcePath();
const cwd = autoBuildSource || process.cwd();
// Load environment variables from auto-claude .env file
// Load environment variables from auto-claude .env file and project settings
const autoBuildEnv = this.loadAutoBuildEnv();
const projectEnv = this.getProjectEnvVars(projectPath);
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
const childProcess = spawn(this.pythonPath, args, {
cwd,
env: {
...process.env,
...autoBuildEnv, // Include auto-claude .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
...combinedEnv, // Include auto-claude .env variables and project-specific env vars
PYTHONUNBUFFERED: '1'
}
});
@@ -558,7 +588,7 @@ export class AgentManager extends EventEmitter {
*/
private spawnRoadmapProcess(
projectId: string,
_projectPath: string,
projectPath: string,
args: string[]
): void {
// Kill existing process for this project if any
@@ -568,14 +598,16 @@ export class AgentManager extends EventEmitter {
const autoBuildSource = this.getAutoBuildSourcePath();
const cwd = autoBuildSource || process.cwd();
// Load environment variables from auto-claude .env file
// Load environment variables from auto-claude .env file and project settings
const autoBuildEnv = this.loadAutoBuildEnv();
const projectEnv = this.getProjectEnvVars(projectPath);
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
const childProcess = spawn(this.pythonPath, args, {
cwd,
env: {
...process.env,
...autoBuildEnv, // Include auto-claude .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
...combinedEnv, // Include auto-claude .env variables and project-specific env vars
PYTHONUNBUFFERED: '1'
}
});
+39 -30
View File
@@ -177,7 +177,7 @@ export class ChangelogService extends EventEmitter {
const specsDir = path.join(projectPath, specsBaseDir || AUTO_BUILD_PATHS.SPECS_DIR);
return tasks
.filter(task => task.status === 'done')
.filter(task => task.status === 'done' && !task.metadata?.archivedAt)
.map(task => {
const specDir = path.join(specsDir, task.specId);
const hasSpecs = existsSync(specDir) && existsSync(path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE));
@@ -548,7 +548,7 @@ ${taskSummaries}
${request.customInstructions ? `Note: ${request.customInstructions}` : ''}
Generate the changelog now. Output ONLY the formatted changelog, no preamble.`;
CRITICAL: Output ONLY the raw changelog content. Do NOT include ANY introductory text, analysis, or explanation. Start directly with the changelog heading (## or #). No "Here's the changelog" or similar phrases.`;
}
/**
@@ -567,37 +567,23 @@ Generate the changelog now. Output ONLY the formatted changelog, no preamble.`;
return `
import subprocess
import sys
import os
print("SCRIPT_START", file=sys.stderr, flush=True)
print(f"HOME={os.environ.get('HOME')}", file=sys.stderr, flush=True)
print(f"Claude path: ${escapedClaudePath}", file=sys.stderr, flush=True)
prompt = """${escapedPrompt}"""
print(f"Prompt length: {len(prompt)}", file=sys.stderr, flush=True)
print("Starting subprocess.run...", file=sys.stderr, flush=True)
# Use Claude Code CLI to generate
# --max-turns 1: Single response (no back-and-forth needed)
# --model haiku: Faster model for simple text generation
try:
result = subprocess.run(
['${escapedClaudePath}', '-p', prompt, '--output-format', 'text', '--max-turns', '1', '--model', 'haiku'],
capture_output=True,
text=True,
timeout=180
)
print(f"subprocess.run completed with code {result.returncode}", file=sys.stderr, flush=True)
# stdin=DEVNULL prevents hanging when claude checks for interactive input
result = subprocess.run(
['${escapedClaudePath}', '-p', prompt, '--output-format', 'text', '--model', 'haiku'],
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
timeout=300
)
if result.returncode == 0:
print(result.stdout)
else:
print(f"STDERR from claude: {result.stderr}", file=sys.stderr, flush=True)
print(result.stderr, file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Exception: {type(e).__name__}: {e}", file=sys.stderr, flush=True)
if result.returncode == 0:
print(result.stdout)
else:
print(result.stderr, file=sys.stderr)
sys.exit(1)
`;
}
@@ -610,11 +596,34 @@ except Exception as e:
// Clean up any potential wrapper text
let changelog = output.trim();
// Remove any "Here's the changelog:" or similar prefixes
// Find where the actual changelog starts (look for markdown heading)
// This handles cases where AI includes preamble like "I'll analyze..." or "Here's the changelog:"
const changelogStartPatterns = [
/^(##\s*\[[\d.]+\])/m, // Keep-a-changelog: ## [1.0.0]
/^(##\s*What['']?s\s+New)/im, // GitHub release: ## What's New
/^(#\s*Release\s+v?[\d.]+)/im, // Simple: # Release v1.0.0
/^(#\s*Changelog)/im, // # Changelog
/^(##\s*v?[\d.]+)/m // ## v1.0.0 or ## 1.0.0
];
for (const pattern of changelogStartPatterns) {
const match = changelog.match(pattern);
if (match && match.index !== undefined) {
// Found a changelog heading - extract from there
changelog = changelog.substring(match.index);
break;
}
}
// Additional cleanup - remove common AI preambles if they somehow remain
const prefixes = [
/^I['']ll\s+analyze[^#]*(?=#)/is,
/^I['']ll\s+generate[^#]*(?=#)/is,
/^Here['']s the changelog[:\s]*/i,
/^The changelog[:\s]*/i,
/^Changelog[:\s]*/i
/^Changelog[:\s]*/i,
/^Based on[^#]*(?=#)/is,
/^Let me[^#]*(?=#)/is
];
for (const prefix of prefixes) {
+152
View File
@@ -1913,6 +1913,40 @@ export function setupIpcHandlers(
}
);
// ============================================
// Task Archive Operations
// ============================================
ipcMain.handle(
IPC_CHANNELS.TASK_ARCHIVE,
async (_, projectId: string, taskIds: string[], version?: string): Promise<IPCResult<boolean>> => {
try {
const success = projectStore.archiveTasks(projectId, taskIds, version);
return { success, data: success };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to archive tasks'
};
}
}
);
ipcMain.handle(
IPC_CHANNELS.TASK_UNARCHIVE,
async (_, projectId: string, taskIds: string[]): Promise<IPCResult<boolean>> => {
try {
const success = projectStore.unarchiveTasks(projectId, taskIds);
return { success, data: success };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to unarchive tasks'
};
}
}
);
// ============================================
// Task Phase Logs (collapsible by phase)
// ============================================
@@ -4422,6 +4456,77 @@ ${issue.body || 'No description provided.'}
}
);
/**
* Create a GitHub release using the gh CLI
*/
ipcMain.handle(
IPC_CHANNELS.GITHUB_CREATE_RELEASE,
async (
_,
projectId: string,
version: string,
releaseNotes: string,
options?: { draft?: boolean; prerelease?: boolean }
): Promise<IPCResult<{ url: string }>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
try {
// Check if gh CLI is available
try {
execSync('which gh', { encoding: 'utf-8' });
} catch {
return {
success: false,
error: 'GitHub CLI (gh) not found. Please install it: https://cli.github.com/'
};
}
// Check if user is authenticated
try {
execSync('gh auth status', { cwd: project.path, encoding: 'utf-8', stdio: 'pipe' });
} catch {
return {
success: false,
error: 'Not authenticated with GitHub. Run "gh auth login" in terminal first.'
};
}
// Prepare tag name (ensure v prefix)
const tag = version.startsWith('v') ? version : `v${version}`;
// Build gh release command
const args = ['release', 'create', tag, '--title', tag, '--notes', releaseNotes];
if (options?.draft) args.push('--draft');
if (options?.prerelease) args.push('--prerelease');
// Create the release
const output = execSync(`gh ${args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(' ')}`, {
cwd: project.path,
encoding: 'utf-8',
stdio: 'pipe'
}).trim();
// Output is typically the release URL
const releaseUrl = output || `https://github.com/releases/tag/${tag}`;
return {
success: true,
data: { url: releaseUrl }
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to create release';
// Try to extract more useful error message from stderr
if (error && typeof error === 'object' && 'stderr' in error) {
return { success: false, error: String(error.stderr) || errorMsg };
}
return { success: false, error: errorMsg };
}
}
);
// ============================================
// Auto Claude Source Update Operations
// ============================================
@@ -5376,6 +5481,53 @@ ${idea.rationale}
}
);
ipcMain.handle(
IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION,
async (_, projectId: string, taskIds: string[]): Promise<IPCResult<{ version: string; reason: string }>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
try {
// Get current version from existing changelog
const existing = changelogService.readExistingChangelog(project.path);
const currentVersion = existing.lastVersion;
// Load specs for selected tasks to analyze change types
const tasks = projectStore.getTasks(projectId);
const devMode = project.settings.devMode ?? false;
const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode);
const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks, specsBaseDir);
// Analyze specs and suggest version
const suggestedVersion = changelogService.suggestVersion(specs, currentVersion);
// Determine reason for the suggestion
let reason = 'patch';
if (currentVersion) {
const [oldMajor, oldMinor] = currentVersion.split('.').map(Number);
const [newMajor, newMinor] = suggestedVersion.split('.').map(Number);
if (newMajor > oldMajor) {
reason = 'breaking';
} else if (newMinor > oldMinor) {
reason = 'feature';
}
}
return {
success: true,
data: { version: suggestedVersion, reason }
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to suggest version'
};
}
}
);
// ============================================
// Changelog Agent Events → Renderer
// ============================================
+73
View File
@@ -366,6 +366,79 @@ export class ProjectStore {
return { status: calculatedStatus, reviewReason: calculatedStatus === 'human_review' ? reviewReason : undefined };
}
/**
* Archive tasks by writing archivedAt to their metadata
* @param projectId - Project ID
* @param taskIds - IDs of tasks to archive
* @param version - Version they were archived in (optional)
*/
archiveTasks(projectId: string, taskIds: string[], version?: string): boolean {
const project = this.getProject(projectId);
if (!project) return false;
const devMode = project.settings.devMode ?? false;
const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode);
const specsDir = path.join(project.path, specsBaseDir);
const archivedAt = new Date().toISOString();
for (const taskId of taskIds) {
const specPath = path.join(specsDir, taskId);
const metadataPath = path.join(specPath, 'task_metadata.json');
try {
let metadata: TaskMetadata = {};
if (existsSync(metadataPath)) {
metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
}
// Add archive info
metadata.archivedAt = archivedAt;
if (version) {
metadata.archivedInVersion = version;
}
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
} catch {
// Continue with other tasks even if one fails
}
}
return true;
}
/**
* Unarchive tasks by removing archivedAt from their metadata
* @param projectId - Project ID
* @param taskIds - IDs of tasks to unarchive
*/
unarchiveTasks(projectId: string, taskIds: string[]): boolean {
const project = this.getProject(projectId);
if (!project) return false;
const devMode = project.settings.devMode ?? false;
const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode);
const specsDir = path.join(project.path, specsBaseDir);
for (const taskId of taskIds) {
const specPath = path.join(specsDir, taskId);
const metadataPath = path.join(specPath, 'task_metadata.json');
try {
if (existsSync(metadataPath)) {
const metadata: TaskMetadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
delete metadata.archivedAt;
delete metadata.archivedInVersion;
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
}
} catch {
// Continue with other tasks even if one fails
}
}
return true;
}
}
// Singleton instance
+20
View File
@@ -178,6 +178,12 @@ const electronAPI: ElectronAPI = {
listWorktrees: (projectId: string): Promise<IPCResult<import('../shared/types').WorktreeListResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_LIST_WORKTREES, projectId),
archiveTasks: (projectId: string, taskIds: string[], version?: string): Promise<IPCResult<boolean>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_ARCHIVE, projectId, taskIds, version),
unarchiveTasks: (projectId: string, taskIds: string[]): Promise<IPCResult<boolean>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_UNARCHIVE, projectId, taskIds),
// ============================================
// Event Listeners (main → renderer)
// ============================================
@@ -557,6 +563,14 @@ const electronAPI: ElectronAPI = {
importGitHubIssues: (projectId: string, issueNumbers: number[]): Promise<IPCResult<GitHubImportResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.GITHUB_IMPORT_ISSUES, projectId, issueNumbers),
createGitHubRelease: (
projectId: string,
version: string,
releaseNotes: string,
options?: { draft?: boolean; prerelease?: boolean }
): Promise<IPCResult<{ url: string }>> =>
ipcRenderer.invoke(IPC_CHANNELS.GITHUB_CREATE_RELEASE, projectId, version, releaseNotes, options),
// ============================================
// GitHub Event Listeners
// ============================================
@@ -792,6 +806,12 @@ const electronAPI: ElectronAPI = {
readExistingChangelog: (projectId: string): Promise<IPCResult<ExistingChangelog>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_READ_EXISTING, projectId),
suggestChangelogVersion: (
projectId: string,
taskIds: string[]
): Promise<IPCResult<{ version: string; reason: string }>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION, projectId, taskIds),
// ============================================
// Changelog Event Listeners
// ============================================
File diff suppressed because it is too large Load Diff
@@ -123,7 +123,7 @@ export function ChatHistorySidebar({
</div>
{/* Session list */}
<ScrollArea className="flex-1">
<ScrollArea className="flex-1 [&>div>div]:!overflow-x-hidden">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
@@ -247,7 +247,7 @@ function SessionItem({
return (
<div
className={cn(
'group flex cursor-pointer items-center gap-2 px-3 py-2 transition-colors hover:bg-muted',
'group flex cursor-pointer items-center gap-2 px-2 py-2 transition-colors hover:bg-muted',
isActive && 'bg-primary/10 hover:bg-primary/15'
)}
onClick={onSelect}
@@ -276,7 +276,7 @@ function SessionItem({
<Button
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
className="h-6 w-6 shrink-0 opacity-70 hover:opacity-100"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-3.5 w-3.5" />
@@ -17,9 +17,11 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy
} from '@dnd-kit/sortable';
import { Plus, Inbox, Loader2, Eye, CheckCircle2 } from 'lucide-react';
import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive } from 'lucide-react';
import { ScrollArea } from './ui/scroll-area';
import { Button } from './ui/button';
import { Checkbox } from './ui/checkbox';
import { Label } from './ui/label';
import { TaskCard } from './TaskCard';
import { SortableTaskCard } from './SortableTaskCard';
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
@@ -196,6 +198,20 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro
export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardProps) {
const [activeTask, setActiveTask] = useState<Task | null>(null);
const [overColumnId, setOverColumnId] = useState<string | null>(null);
const [showArchived, setShowArchived] = useState(false);
// Count archived tasks for display
const archivedCount = useMemo(() => {
return tasks.filter((t) => t.metadata?.archivedAt).length;
}, [tasks]);
// Filter tasks based on archive status
const filteredTasks = useMemo(() => {
if (showArchived) {
return tasks; // Show all tasks including archived
}
return tasks.filter((t) => !t.metadata?.archivedAt);
}, [tasks, showArchived]);
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -217,14 +233,14 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
done: []
};
tasks.forEach((task) => {
filteredTasks.forEach((task) => {
if (grouped[task.status]) {
grouped[task.status].push(task);
}
});
return grouped;
}, [tasks]);
}, [filteredTasks]);
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
@@ -291,34 +307,60 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex h-full gap-4 overflow-x-auto p-6">
{TASK_STATUS_COLUMNS.map((status) => (
<DroppableColumn
key={status}
status={status}
tasks={tasksByStatus[status]}
onTaskClick={onTaskClick}
isOver={overColumnId === status}
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
<div className="flex h-full flex-col">
{/* Kanban header with filters */}
<div className="flex items-center justify-end px-6 py-3 border-b border-border/50">
<div className="flex items-center gap-2">
<Checkbox
id="showArchived"
checked={showArchived}
onCheckedChange={(checked) => setShowArchived(checked === true)}
/>
))}
<Label
htmlFor="showArchived"
className="flex items-center gap-1.5 text-sm text-muted-foreground cursor-pointer"
>
<Archive className="h-3.5 w-3.5" />
Show archived
{archivedCount > 0 && (
<span className="ml-1 text-xs px-1.5 py-0.5 rounded-full bg-muted">
{archivedCount}
</span>
)}
</Label>
</div>
</div>
{/* Drag overlay - enhanced visual feedback */}
<DragOverlay>
{activeTask ? (
<div className="drag-overlay-card">
<TaskCard task={activeTask} onClick={() => {}} />
</div>
) : null}
</DragOverlay>
</DndContext>
{/* Kanban columns */}
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex flex-1 gap-4 overflow-x-auto p-6">
{TASK_STATUS_COLUMNS.map((status) => (
<DroppableColumn
key={status}
status={status}
tasks={tasksByStatus[status]}
onTaskClick={onTaskClick}
isOver={overColumnId === status}
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
/>
))}
</div>
{/* Drag overlay - enhanced visual feedback */}
<DragOverlay>
{activeTask ? (
<div className="drag-overlay-card">
<TaskCard task={activeTask} onClick={() => {}} />
</div>
) : null}
</DragOverlay>
</DndContext>
</div>
);
}
@@ -961,6 +961,44 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings
</p>
</div>
{/* Graphiti MCP Server Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="font-normal text-foreground">Enable Agent Memory Access</Label>
<p className="text-xs text-muted-foreground">
Allow agents to search and add to the knowledge graph via MCP
</p>
</div>
<Switch
checked={settings.graphitiMcpEnabled}
onCheckedChange={(checked) =>
setSettings({ ...settings, graphitiMcpEnabled: checked })
}
/>
</div>
{settings.graphitiMcpEnabled && (
<div className="space-y-2 ml-6">
<Label className="text-sm font-medium text-foreground">Graphiti MCP Server URL</Label>
<p className="text-xs text-muted-foreground">
URL of the Graphiti MCP server (requires Docker container)
</p>
<Input
placeholder="http://localhost:8000/mcp/"
value={settings.graphitiMcpUrl || ''}
onChange={(e) => setSettings({ ...settings, graphitiMcpUrl: e.target.value || undefined })}
/>
<div className="rounded-lg border border-info/30 bg-info/5 p-3">
<p className="text-xs text-info">
Start the MCP server with:{' '}
<code className="px-1 bg-info/10 rounded">docker run -d -p 8000:8000 falkordb/graphiti-knowledge-graph-mcp</code>
</p>
</div>
</div>
)}
<Separator />
{/* LLM Provider Selection - V2 Multi-provider support */}
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">LLM Provider</Label>
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw } from 'lucide-react';
import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive } from 'lucide-react';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Progress } from './ui/progress';
@@ -133,12 +133,15 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
const reviewReasonInfo = task.status === 'human_review' ? getReviewReasonLabel(task.reviewReason) : null;
const isArchived = !!task.metadata?.archivedAt;
return (
<Card
className={cn(
'card-surface task-card-enhanced cursor-pointer',
isRunning && !isStuck && 'ring-2 ring-primary border-primary task-running-pulse',
isStuck && 'ring-2 ring-warning border-warning task-stuck-pulse'
isStuck && 'ring-2 ring-warning border-warning task-stuck-pulse',
isArchived && 'opacity-60 hover:opacity-80'
)}
onClick={onClick}
>
@@ -172,6 +175,16 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
Incomplete
</Badge>
)}
{/* Archived indicator - task has been released */}
{task.metadata?.archivedAt && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 flex items-center gap-1 bg-muted text-muted-foreground border-border"
>
<Archive className="h-2.5 w-2.5" />
Archived
</Badge>
)}
{/* Execution phase badge - shown when actively running */}
{hasActiveExecution && executionPhase && !isStuck && !isIncomplete && (
<Badge
+8 -1
View File
@@ -113,7 +113,10 @@ export const DEFAULT_PROJECT_SETTINGS = {
onReviewNeeded: true,
sound: false
},
devMode: false
devMode: false,
// Graphiti MCP server for agent-accessible knowledge graph (enabled by default)
graphitiMcpEnabled: true,
graphitiMcpUrl: 'http://localhost:8000/mcp/'
};
// IPC Channel names
@@ -146,6 +149,8 @@ export const IPC_CHANNELS = {
TASK_WORKTREE_MERGE: 'task:worktreeMerge',
TASK_WORKTREE_DISCARD: 'task:worktreeDiscard',
TASK_LIST_WORKTREES: 'task:listWorktrees',
TASK_ARCHIVE: 'task:archive',
TASK_UNARCHIVE: 'task:unarchive',
// Task events (main -> renderer)
TASK_PROGRESS: 'task:progress',
@@ -238,6 +243,7 @@ export const IPC_CHANNELS = {
GITHUB_CHECK_CONNECTION: 'github:checkConnection',
GITHUB_INVESTIGATE_ISSUE: 'github:investigateIssue',
GITHUB_IMPORT_ISSUES: 'github:importIssues',
GITHUB_CREATE_RELEASE: 'github:createRelease',
// GitHub events (main -> renderer)
GITHUB_INVESTIGATION_PROGRESS: 'github:investigationProgress',
@@ -261,6 +267,7 @@ export const IPC_CHANNELS = {
CHANGELOG_GENERATE: 'changelog:generate',
CHANGELOG_SAVE: 'changelog:save',
CHANGELOG_READ_EXISTING: 'changelog:readExisting',
CHANGELOG_SUGGEST_VERSION: 'changelog:suggestVersion',
// Changelog events (main -> renderer)
CHANGELOG_GENERATION_PROGRESS: 'changelog:generationProgress',
+22
View File
@@ -23,6 +23,10 @@ export interface ProjectSettings {
notifications: NotificationSettings;
/** Dev mode: use dev/auto-claude/specs/ for framework development */
devMode: boolean;
/** Enable Graphiti MCP server for agent-accessible knowledge graph */
graphitiMcpEnabled: boolean;
/** Graphiti MCP server URL (default: http://localhost:8000/mcp/) */
graphitiMcpUrl?: string;
}
export interface NotificationSettings {
@@ -204,6 +208,10 @@ export interface TaskMetadata {
// Review settings
requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts
// Archive status
archivedAt?: string; // ISO date when task was archived
archivedInVersion?: string; // Version in which task was archived (from changelog)
}
export interface Task {
@@ -1201,6 +1209,10 @@ export interface ElectronAPI {
discardWorktree: (taskId: string) => Promise<IPCResult<WorktreeDiscardResult>>;
listWorktrees: (projectId: string) => Promise<IPCResult<WorktreeListResult>>;
// Task archive operations
archiveTasks: (projectId: string, taskIds: string[], version?: string) => Promise<IPCResult<boolean>>;
unarchiveTasks: (projectId: string, taskIds: string[]) => Promise<IPCResult<boolean>>;
// Event listeners
onTaskProgress: (callback: (taskId: string, plan: ImplementationPlan) => void) => () => void;
onTaskError: (callback: (taskId: string, error: string) => void) => () => void;
@@ -1282,6 +1294,12 @@ export interface ElectronAPI {
checkGitHubConnection: (projectId: string) => Promise<IPCResult<GitHubSyncStatus>>;
investigateGitHubIssue: (projectId: string, issueNumber: number) => void;
importGitHubIssues: (projectId: string, issueNumbers: number[]) => Promise<IPCResult<GitHubImportResult>>;
createGitHubRelease: (
projectId: string,
version: string,
releaseNotes: string,
options?: { draft?: boolean; prerelease?: boolean }
) => Promise<IPCResult<{ url: string }>>;
// GitHub event listeners
onGitHubInvestigationProgress: (
@@ -1343,6 +1361,10 @@ export interface ElectronAPI {
generateChangelog: (request: ChangelogGenerationRequest) => void; // Async with progress events
saveChangelog: (request: ChangelogSaveRequest) => Promise<IPCResult<ChangelogSaveResult>>;
readExistingChangelog: (projectId: string) => Promise<IPCResult<ExistingChangelog>>;
suggestChangelogVersion: (
projectId: string,
taskIds: string[]
) => Promise<IPCResult<{ version: string; reason: string }>>;
// Changelog event listeners
onChangelogGenerationProgress: (
+23
View File
@@ -172,6 +172,29 @@ CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here
# Common values: nomic-embed-text=768, mxbai-embed-large=1024, all-minilm=384
# OLLAMA_EMBEDDING_DIM=768
# =============================================================================
# GRAPHITI MCP SERVER (OPTIONAL - Agent-accessible knowledge graph)
# =============================================================================
# Enable Graphiti MCP server for direct agent access to knowledge graph.
# This is SEPARATE from GRAPHITI_ENABLED (Python library integration).
#
# With MCP server enabled, Claude agents can directly:
# - Search for relevant context (search_nodes, search_facts)
# - Add discoveries to the graph (add_episode)
# - Retrieve recent sessions (get_episodes)
#
# Prerequisites:
# 1. Run the Graphiti MCP server via Docker:
# docker run -d -p 8000:8000 \
# -e OPENAI_API_KEY=$OPENAI_API_KEY \
# -e FALKORDB_URI=redis://host.docker.internal:6380 \
# falkordb/graphiti-knowledge-graph-mcp
#
# See: https://docs.falkordb.com/agentic-memory/graphiti-mcp-server.html
# Graphiti MCP Server URL (setting this enables MCP integration)
# GRAPHITI_MCP_URL=http://localhost:8000/mcp/
# =============================================================================
# GRAPHITI: FalkorDB Connection Settings
# =============================================================================
+42
View File
@@ -21,6 +21,21 @@ from auto_claude_tools import (
)
def is_graphiti_mcp_enabled() -> bool:
"""
Check if Graphiti MCP server integration is enabled.
Requires GRAPHITI_MCP_URL to be set (e.g., http://localhost:8000/mcp/)
This is separate from GRAPHITI_ENABLED which controls the Python library integration.
"""
return bool(os.environ.get("GRAPHITI_MCP_URL"))
def get_graphiti_mcp_url() -> str:
"""Get the Graphiti MCP server URL."""
return os.environ.get("GRAPHITI_MCP_URL", "http://localhost:8000/mcp/")
# Puppeteer MCP tools for browser automation
PUPPETEER_TOOLS = [
"mcp__puppeteer__puppeteer_connect_active_tab",
@@ -59,6 +74,16 @@ CONTEXT7_TOOLS = [
"mcp__context7__get-library-docs",
]
# Graphiti MCP tools for knowledge graph memory (when GRAPHITI_MCP_ENABLED is set)
# See: https://docs.falkordb.com/agentic-memory/graphiti-mcp-server.html
GRAPHITI_MCP_TOOLS = [
"mcp__graphiti-memory__search_nodes", # Search entity summaries
"mcp__graphiti-memory__search_facts", # Search relationships between entities
"mcp__graphiti-memory__add_episode", # Add data to knowledge graph
"mcp__graphiti-memory__get_episodes", # Retrieve recent episodes
"mcp__graphiti-memory__get_entity_edge", # Get specific entity/relationship
]
# Built-in tools
BUILTIN_TOOLS = [
"Read",
@@ -117,11 +142,16 @@ def create_client(
else:
allowed_tools_list = [*BUILTIN_TOOLS]
# Check if Graphiti MCP is enabled
graphiti_mcp_enabled = is_graphiti_mcp_enabled()
# Add external MCP tools
allowed_tools_list.extend(PUPPETEER_TOOLS)
allowed_tools_list.extend(CONTEXT7_TOOLS)
if linear_enabled:
allowed_tools_list.extend(LINEAR_TOOLS)
if graphiti_mcp_enabled:
allowed_tools_list.extend(GRAPHITI_MCP_TOOLS)
# Create comprehensive security settings
# Note: Using relative paths ("./**") restricts access to project directory
@@ -146,6 +176,8 @@ def create_client(
*CONTEXT7_TOOLS,
# Allow Linear MCP tools for project management (if enabled)
*(LINEAR_TOOLS if linear_enabled else []),
# Allow Graphiti MCP tools for knowledge graph memory (if enabled)
*(GRAPHITI_MCP_TOOLS if graphiti_mcp_enabled else []),
],
},
}
@@ -163,6 +195,8 @@ def create_client(
mcp_servers_list = ["puppeteer (browser automation)", "context7 (documentation)"]
if linear_enabled:
mcp_servers_list.append("linear (project management)")
if graphiti_mcp_enabled:
mcp_servers_list.append("graphiti-memory (knowledge graph)")
if auto_claude_tools_enabled:
mcp_servers_list.append(f"auto-claude ({agent_type} tools)")
print(f" - MCP servers: {', '.join(mcp_servers_list)}")
@@ -182,6 +216,14 @@ def create_client(
"headers": {"Authorization": f"Bearer {linear_api_key}"}
}
# Add Graphiti MCP server if enabled
# Requires running: docker run -d -p 8000:8000 falkordb/graphiti-knowledge-graph-mcp
if graphiti_mcp_enabled:
mcp_servers["graphiti-memory"] = {
"type": "http",
"url": get_graphiti_mcp_url(),
}
# Add custom auto-claude MCP server if available
auto_claude_mcp_server = None
if auto_claude_tools_enabled: