claude-ws 0.4.9 → 0.5.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/locales/de.json +13 -0
  2. package/locales/en.json +20 -0
  3. package/locales/es.json +13 -0
  4. package/locales/fr.json +13 -0
  5. package/locales/ja.json +13 -0
  6. package/locales/ko.json +13 -0
  7. package/locales/vi.json +20 -0
  8. package/locales/zh.json +13 -0
  9. package/package.json +1 -1
  10. package/packages/agentic-sdk/src/agent/claude-sdk-agent-provider.ts +4 -3
  11. package/packages/agentic-sdk/src/services/task/task-conversation-history-builder.ts +1 -1
  12. package/packages/agentic-sdk/src/services/task/task-crud-and-reorder.ts +15 -1
  13. package/server.ts +165 -13
  14. package/src/app/[locale]/page.tsx +21 -2
  15. package/src/app/api/butler/schedules/[id]/route.ts +84 -0
  16. package/src/app/api/butler/schedules/route.ts +81 -0
  17. package/src/app/api/tasks/[id]/route.ts +2 -1
  18. package/src/app/api/tasks/reorder/route.ts +2 -1
  19. package/src/app/api/tasks/route.ts +2 -1
  20. package/src/app/globals.css +3 -0
  21. package/src/components/butler/butler-status-indicator.tsx +235 -0
  22. package/src/components/header.tsx +4 -0
  23. package/src/components/kanban/board.tsx +5 -2
  24. package/src/components/kanban/task-card-context-menu.tsx +2 -1
  25. package/src/components/kanban/task-card.tsx +8 -3
  26. package/src/components/providers/socket-provider.tsx +100 -0
  27. package/src/components/sidebar/file-browser/use-file-tab-search.ts +35 -12
  28. package/src/components/task/attempt-list.tsx +3 -5
  29. package/src/components/task/conversation-view-historical-user-turn.tsx +4 -1
  30. package/src/components/task/conversation-view-streaming-prompt-bubble.tsx +1 -1
  31. package/src/components/task/conversation-view.tsx +23 -10
  32. package/src/components/task/floating-chat-window.tsx +31 -29
  33. package/src/components/task/interactive-command/interactive-command-overlay.tsx +2 -6
  34. package/src/components/task/task-detail-panel.tsx +3 -1
  35. package/src/components/task/use-task-attempt-stream-handler.ts +14 -2
  36. package/src/components/task/use-task-stats.ts +11 -7
  37. package/src/components/team-view/team-view.tsx +3 -3
  38. package/src/components/ui/detachable-window.tsx +4 -4
  39. package/src/hooks/use-attempt-questions.ts +5 -9
  40. package/src/hooks/use-attempt-socket.ts +38 -1
  41. package/src/hooks/use-attempt-stream-running-attempt-utils.ts +6 -10
  42. package/src/hooks/use-attempt-stream.ts +51 -2
  43. package/src/hooks/use-butler-notifications.ts +34 -0
  44. package/src/hooks/use-butler.ts +25 -0
  45. package/src/hooks/use-kanban-url-sync-and-deep-links.ts +2 -8
  46. package/src/lib/agent-manager.ts +10 -6
  47. package/src/lib/autopilot/autopilot-manager.ts +43 -30
  48. package/src/lib/butler/butler-action-executor.ts +112 -0
  49. package/src/lib/butler/butler-attempt-resumption-service.ts +137 -0
  50. package/src/lib/butler/butler-decision-loop.ts +89 -0
  51. package/src/lib/butler/butler-event-collector.ts +42 -0
  52. package/src/lib/butler/butler-lifecycle-service.ts +84 -0
  53. package/src/lib/butler/butler-manager.ts +231 -0
  54. package/src/lib/butler/butler-memory-manager.ts +116 -0
  55. package/src/lib/butler/butler-notification-service.ts +70 -0
  56. package/src/lib/butler/butler-persona-loader.ts +47 -0
  57. package/src/lib/butler/butler-project-initializer.ts +179 -0
  58. package/src/lib/butler/butler-prompt-builder.ts +114 -0
  59. package/src/lib/butler/butler-rule-engine.ts +242 -0
  60. package/src/lib/butler/butler-scheduler-service.ts +336 -0
  61. package/src/lib/butler/butler-session-spawner.ts +134 -0
  62. package/src/lib/butler/butler-types.ts +110 -0
  63. package/src/lib/butler/butler-workspace-api.ts +122 -0
  64. package/src/lib/butler/index.ts +22 -0
  65. package/src/lib/butler-manager-singleton.ts +21 -0
  66. package/src/lib/services/task-api-service.ts +252 -0
  67. package/src/lib/services/task-service-with-socket-emit.ts +51 -0
  68. package/src/lib/socket-io-server-singleton.ts +15 -0
  69. package/src/stores/butler-store.ts +68 -0
  70. package/src/stores/model-store.ts +16 -27
  71. package/src/stores/task-store-api-actions.ts +9 -23
  72. package/src/stores/task-store-mutation-api-actions.ts +6 -30
package/locales/de.json CHANGED
@@ -617,6 +617,7 @@
617
617
  "cancelled": "Abgebrochen",
618
618
  "detachToFloating": "In schwebendes Fenster abtrennen",
619
619
  "maximizeToPanel": "Als Panel maximieren",
620
+ "sendError": "Senden fehlgeschlagen",
620
621
  "clearConversation": "Unterhaltung löschen",
621
622
  "clearConversationConfirm": "Sind Sie sicher, dass Sie alle Nachrichten löschen möchten? Dies kann nicht rückgängig gemacht werden.",
622
623
  "compactConversation": "Unterhaltung komprimieren",
@@ -787,6 +788,18 @@
787
788
  "description": "Die gesuchte Seite existiert nicht.",
788
789
  "goHome": "Zurück zur Startseite"
789
790
  },
791
+ "butler": {
792
+ "disabled": "Butler deaktiviert",
793
+ "status_idle": "Butler inaktiv",
794
+ "status_initializing": "Butler startet...",
795
+ "status_running": "Butler aktiv",
796
+ "status_reasoning": "Butler denkt nach...",
797
+ "status_shutting_down": "Butler wird gestoppt...",
798
+ "enable": "Butler aktivieren",
799
+ "disable": "Butler deaktivieren",
800
+ "openProject": "Butler-Projekt öffnen",
801
+ "startTask": "Butler Task"
802
+ },
790
803
  "globalError": {
791
804
  "title": "Etwas ist schiefgelaufen!",
792
805
  "fallbackMessage": "Ein unerwarteter Fehler ist aufgetreten",
package/locales/en.json CHANGED
@@ -619,6 +619,7 @@
619
619
  "cancelled": "Cancelled",
620
620
  "detachToFloating": "Detach to floating window",
621
621
  "maximizeToPanel": "Maximize to panel",
622
+ "sendError": "Failed to send",
622
623
  "clearConversation": "Clear Conversation",
623
624
  "clearConversationConfirm": "Are you sure you want to clear all messages? This cannot be undone.",
624
625
  "compactConversation": "Compact Conversation",
@@ -792,6 +793,25 @@
792
793
  "description": "The page you're looking for doesn't exist.",
793
794
  "goHome": "Go back home"
794
795
  },
796
+ "butler": {
797
+ "disabled": "Butler disabled",
798
+ "status_idle": "Butler idle",
799
+ "status_initializing": "Butler starting...",
800
+ "status_running": "Butler active",
801
+ "status_reasoning": "Butler thinking...",
802
+ "status_shutting_down": "Butler stopping...",
803
+ "enable": "Enable Butler",
804
+ "disable": "Disable Butler",
805
+ "openProject": "Open Butler Project",
806
+ "notifications": "Notifications",
807
+ "noNotifications": "No notifications",
808
+ "clearAll": "Clear all",
809
+ "justNow": "just now",
810
+ "minutesAgo": "{m}m ago",
811
+ "hoursAgo": "{h}h ago",
812
+ "daysAgo": "{d}d ago",
813
+ "startTask": "Butler Task"
814
+ },
795
815
  "globalError": {
796
816
  "title": "Something went wrong!",
797
817
  "fallbackMessage": "An unexpected error occurred",
package/locales/es.json CHANGED
@@ -617,6 +617,7 @@
617
617
  "cancelled": "Cancelado",
618
618
  "detachToFloating": "Desacoplar a ventana flotante",
619
619
  "maximizeToPanel": "Maximizar al panel",
620
+ "sendError": "Error al enviar",
620
621
  "clearConversation": "Limpiar Conversación",
621
622
  "clearConversationConfirm": "¿Está seguro de que desea borrar todos los mensajes? Esta acción no se puede deshacer.",
622
623
  "compactConversation": "Compactar Conversación",
@@ -787,6 +788,18 @@
787
788
  "description": "La página que busca no existe.",
788
789
  "goHome": "Volver al inicio"
789
790
  },
791
+ "butler": {
792
+ "disabled": "Butler desactivado",
793
+ "status_idle": "Butler inactivo",
794
+ "status_initializing": "Butler iniciando...",
795
+ "status_running": "Butler activo",
796
+ "status_reasoning": "Butler pensando...",
797
+ "status_shutting_down": "Butler deteniéndose...",
798
+ "enable": "Activar Butler",
799
+ "disable": "Desactivar Butler",
800
+ "openProject": "Abrir proyecto Butler",
801
+ "startTask": "Butler Task"
802
+ },
790
803
  "globalError": {
791
804
  "title": "¡Algo salió mal!",
792
805
  "fallbackMessage": "Ocurrió un error inesperado",
package/locales/fr.json CHANGED
@@ -617,6 +617,7 @@
617
617
  "cancelled": "Annulé",
618
618
  "detachToFloating": "Détacher en fenêtre flottante",
619
619
  "maximizeToPanel": "Maximiser en panneau",
620
+ "sendError": "Échec de l'envoi",
620
621
  "clearConversation": "Effacer la conversation",
621
622
  "clearConversationConfirm": "Êtes-vous sûr de vouloir effacer tous les messages ? Cette action est irréversible.",
622
623
  "compactConversation": "Compacter la conversation",
@@ -787,6 +788,18 @@
787
788
  "description": "La page que vous recherchez n'existe pas.",
788
789
  "goHome": "Retourner à l'accueil"
789
790
  },
791
+ "butler": {
792
+ "disabled": "Butler désactivé",
793
+ "status_idle": "Butler inactif",
794
+ "status_initializing": "Butler en démarrage...",
795
+ "status_running": "Butler actif",
796
+ "status_reasoning": "Butler en réflexion...",
797
+ "status_shutting_down": "Butler en arrêt...",
798
+ "enable": "Activer Butler",
799
+ "disable": "Désactiver Butler",
800
+ "openProject": "Ouvrir le projet Butler",
801
+ "startTask": "Butler Task"
802
+ },
790
803
  "globalError": {
791
804
  "title": "Quelque chose s'est mal passé !",
792
805
  "fallbackMessage": "Une erreur inattendue s'est produite",
package/locales/ja.json CHANGED
@@ -617,6 +617,7 @@
617
617
  "cancelled": "キャンセルされました",
618
618
  "detachToFloating": "フローティングウィンドウにデタッチ",
619
619
  "maximizeToPanel": "パネルに最大化",
620
+ "sendError": "送信に失敗しました",
620
621
  "clearConversation": "会話をクリア",
621
622
  "clearConversationConfirm": "すべてのメッセージをクリアしてもよろしいですか?この操作は元に戻せません。",
622
623
  "compactConversation": "会話をコンパクト化",
@@ -787,6 +788,18 @@
787
788
  "description": "お探しのページは存在しません。",
788
789
  "goHome": "ホームに戻る"
789
790
  },
791
+ "butler": {
792
+ "disabled": "バトラー無効",
793
+ "status_idle": "バトラー待機中",
794
+ "status_initializing": "バトラー起動中...",
795
+ "status_running": "バトラー稼働中",
796
+ "status_reasoning": "バトラー思考中...",
797
+ "status_shutting_down": "バトラー停止中...",
798
+ "enable": "バトラーを有効にする",
799
+ "disable": "バトラーを無効にする",
800
+ "openProject": "バトラープロジェクトを開く",
801
+ "startTask": "Butler Task"
802
+ },
790
803
  "globalError": {
791
804
  "title": "問題が発生しました!",
792
805
  "fallbackMessage": "予期しないエラーが発生しました",
package/locales/ko.json CHANGED
@@ -617,6 +617,7 @@
617
617
  "cancelled": "취소됨",
618
618
  "detachToFloating": "플로팅 창으로 분리",
619
619
  "maximizeToPanel": "패널로 최대화",
620
+ "sendError": "전송 실패",
620
621
  "clearConversation": "대화 지우기",
621
622
  "clearConversationConfirm": "모든 메시지를 지우시겠습니까? 이 작업은 취소할 수 없습니다.",
622
623
  "compactConversation": "대화 압축",
@@ -787,6 +788,18 @@
787
788
  "description": "찾고 있는 페이지가 존재하지 않습니다.",
788
789
  "goHome": "홈으로 돌아가기"
789
790
  },
791
+ "butler": {
792
+ "disabled": "버틀러 비활성화",
793
+ "status_idle": "버틀러 대기 중",
794
+ "status_initializing": "버틀러 시작 중...",
795
+ "status_running": "버틀러 활성",
796
+ "status_reasoning": "버틀러 생각 중...",
797
+ "status_shutting_down": "버틀러 종료 중...",
798
+ "enable": "버틀러 활성화",
799
+ "disable": "버틀러 비활성화",
800
+ "openProject": "버틀러 프로젝트 열기",
801
+ "startTask": "Butler Task"
802
+ },
790
803
  "globalError": {
791
804
  "title": "문제가 발생했습니다!",
792
805
  "fallbackMessage": "예기치 않은 오류가 발생했습니다",
package/locales/vi.json CHANGED
@@ -619,6 +619,7 @@
619
619
  "cancelled": "Đã hủy",
620
620
  "detachToFloating": "Tách thành cửa sổ nổi",
621
621
  "maximizeToPanel": "Phóng to thành bảng",
622
+ "sendError": "Gửi thất bại",
622
623
  "clearConversation": "Xóa cuộc trò chuyện",
623
624
  "clearConversationConfirm": "Bạn có chắc muốn xóa tất cả tin nhắn? Hành động này không thể hoàn tác.",
624
625
  "compactConversation": "Nén cuộc trò chuyện",
@@ -789,6 +790,25 @@
789
790
  "description": "Trang bạn đang tìm không tồn tại.",
790
791
  "goHome": "Quay về trang chủ"
791
792
  },
793
+ "butler": {
794
+ "disabled": "Butler đã tắt",
795
+ "status_idle": "Butler đang rảnh",
796
+ "status_initializing": "Butler đang khởi động...",
797
+ "status_running": "Butler đang hoạt động",
798
+ "status_reasoning": "Butler đang suy nghĩ...",
799
+ "status_shutting_down": "Butler đang dừng...",
800
+ "enable": "Bật Butler",
801
+ "disable": "Tắt Butler",
802
+ "openProject": "Mở dự án Butler",
803
+ "notifications": "Thông báo",
804
+ "noNotifications": "Không có thông báo",
805
+ "clearAll": "Xóa tất cả",
806
+ "justNow": "vừa xong",
807
+ "minutesAgo": "{m} phút trước",
808
+ "hoursAgo": "{h} giờ trước",
809
+ "daysAgo": "{d} ngày trước",
810
+ "startTask": "Butler Task"
811
+ },
792
812
  "globalError": {
793
813
  "title": "Đã xảy ra lỗi!",
794
814
  "fallbackMessage": "Đã xảy ra lỗi không mong đợi",
package/locales/zh.json CHANGED
@@ -617,6 +617,7 @@
617
617
  "cancelled": "已取消",
618
618
  "detachToFloating": "分离为浮动窗口",
619
619
  "maximizeToPanel": "最大化为面板",
620
+ "sendError": "发送失败",
620
621
  "clearConversation": "清除对话",
621
622
  "clearConversationConfirm": "您确定要清除所有消息吗?此操作无法撤销。",
622
623
  "compactConversation": "压缩对话",
@@ -787,6 +788,18 @@
787
788
  "description": "您所查找的页面不存在。",
788
789
  "goHome": "返回首页"
789
790
  },
791
+ "butler": {
792
+ "disabled": "管家已禁用",
793
+ "status_idle": "管家空闲",
794
+ "status_initializing": "管家启动中...",
795
+ "status_running": "管家运行中",
796
+ "status_reasoning": "管家思考中...",
797
+ "status_shutting_down": "管家停止中...",
798
+ "enable": "启用管家",
799
+ "disable": "禁用管家",
800
+ "openProject": "打开管家项目",
801
+ "startTask": "Butler Task"
802
+ },
790
803
  "globalError": {
791
804
  "title": "出现错误!",
792
805
  "fallbackMessage": "发生了意外错误",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-ws",
3
- "version": "0.4.9",
3
+ "version": "0.5.1-beta.1",
4
4
  "private": false,
5
5
  "description": "AI-powered workspace for solo CEOs and indie builders — manage your entire business with AI agents, not just code. Kanban board, code editor, Git integration, claw agent hub, local-first SQLite.",
6
6
  "keywords": [
@@ -11,10 +11,11 @@ import { homedir } from 'os';
11
11
  import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
12
12
  import { createRequire } from 'module';
13
13
 
14
- // Resolve cli.js path explicitly to work around pnpm nested node_modules layout
15
- // where the SDK's built-in dirname-based resolution fails
14
+ // Resolve cli.js path by finding the SDK package directory first, then appending cli.js.
15
+ // Direct subpath resolution fails because cli.js is not in the SDK's exports map.
16
16
  const require_ = createRequire(import.meta.url);
17
- const CLAUDE_CLI_PATH = require_.resolve('@anthropic-ai/claude-agent-sdk/cli.js');
17
+ const sdkMainPath = require_.resolve('@anthropic-ai/claude-agent-sdk');
18
+ const CLAUDE_CLI_PATH = join(sdkMainPath, '..', 'cli.js');
18
19
  import { adaptSDKMessage, isValidSDKMessage } from './claude-sdk-message-to-output-adapter';
19
20
  import type { SDKResultMessage } from './claude-sdk-message-to-output-adapter';
20
21
  import { createLogger } from '../lib/pino-logger';
@@ -36,7 +36,7 @@ export async function buildConversationHistory(db: any, taskId: string): Promise
36
36
 
37
37
  turns.push({
38
38
  type: 'user',
39
- prompt: attempt.displayPrompt || attempt.prompt,
39
+ prompt: attempt.displayPrompt ?? attempt.prompt,
40
40
  messages: [],
41
41
  attemptId: attempt.id,
42
42
  timestamp: attempt.createdAt,
@@ -3,7 +3,7 @@
3
3
  * Read-heavy query methods (conversation history, stats, running attempt) live in
4
4
  * task-attempt-and-conversation-queries.ts and are composed in here.
5
5
  */
6
- import { eq, and, desc, inArray } from 'drizzle-orm';
6
+ import { eq, and, desc, inArray, gte, ne, sql } from 'drizzle-orm';
7
7
  import * as schema from '../../db/database-schema';
8
8
  import { generateId } from '../../lib/nanoid-id-generator';
9
9
  import { createTaskQueryMethods } from './task-attempt-and-conversation-queries';
@@ -85,6 +85,20 @@ export function createTaskService(db: any) {
85
85
  // --- reorder ---
86
86
 
87
87
  async reorder(taskId: string, newPosition: number, newStatus?: string) {
88
+ const task = db.select().from(schema.tasks).where(eq(schema.tasks.id, taskId)).get();
89
+ if (!task) return null;
90
+
91
+ const targetStatus = (newStatus || task.status) as any;
92
+
93
+ // Shift tasks at position >= newPosition up by 1 in the target column
94
+ await db.update(schema.tasks)
95
+ .set({ position: sql`${schema.tasks.position} + 1` })
96
+ .where(and(
97
+ eq(schema.tasks.status, targetStatus),
98
+ gte(schema.tasks.position, newPosition),
99
+ ne(schema.tasks.id, taskId)
100
+ ));
101
+
88
102
  const updates: any = { position: newPosition, updatedAt: Date.now() };
89
103
  if (newStatus) updates.status = newStatus;
90
104
  await db.update(schema.tasks).set(updates).where(eq(schema.tasks.id, taskId));
package/server.ts CHANGED
@@ -47,6 +47,7 @@ const log = createLogger('Server');
47
47
  import { eq } from 'drizzle-orm';
48
48
  import { nanoid } from 'nanoid';
49
49
  import type { AttemptStatus } from './src/types';
50
+ import { setSocketServer } from './src/lib/socket-io-server-singleton';
50
51
  import { processAttachments } from './src/lib/file-processor';
51
52
  import { usageTracker } from './src/lib/usage-tracker';
52
53
  import { workflowTracker } from './src/lib/workflow-tracker';
@@ -56,6 +57,9 @@ import { getMinioPullQueueWorker } from './src/lib/minio-pull-queue';
56
57
  import { getMinioPushQueueWorker } from './src/lib/minio-push-queue';
57
58
  import { createAutopilotManager, appendQuestionAnswer, appendSubagentEnded, appendTrackedTaskUpdate } from './src/lib/autopilot';
58
59
  import type { AutopilotMode } from './src/lib/autopilot';
60
+ import { createButlerManager, type ButlerManager } from './src/lib/butler';
61
+ import { createTaskService } from '@agentic-sdk/services/task/task-crud-and-reorder';
62
+ import { createTaskServiceWithSocketEmit } from './src/lib/services/task-service-with-socket-emit';
59
63
 
60
64
  import { getPort, getHostname } from './src/lib/server-port-configuration';
61
65
 
@@ -141,6 +145,9 @@ app.prepare().then(async () => {
141
145
 
142
146
  log.info(`[Server] Restored ${shellManager.runningCount} running shells`);
143
147
 
148
+ // Initialize task service with auto-emit for Socket.IO events
149
+ const taskService = createTaskServiceWithSocketEmit(createTaskService(db));
150
+
144
151
  // Initialize Autopilot Manager
145
152
  const autopilotManager = createAutopilotManager();
146
153
  autopilotManager.registerQuestionListener(agentManager);
@@ -161,8 +168,24 @@ app.prepare().then(async () => {
161
168
  },
162
169
  });
163
170
 
171
+ // Expose io singleton for Next.js API routes to emit events
172
+ setSocketServer(io);
173
+
164
174
  // Restore autopilot state from DB (needs io for worker callbacks)
165
- await autopilotManager.restoreFromDb({ db, io, schema, agentManager, sessionManager });
175
+ await autopilotManager.restoreFromDb({ db, io, schema, agentManager, sessionManager, taskService });
176
+
177
+ // Initialize Butler Agent (workspace-wide orchestrator)
178
+ // NOTE: Do not await — Butler spawns SDK sessions that hit our proxy endpoint,
179
+ // which requires httpServer.listen() to complete first. Fire and forget to avoid deadlock.
180
+ const butlerManager = createButlerManager();
181
+
182
+ // Export butlerManager singleton for API routes to access
183
+ const globalKey = '__claude_butler_manager__' as const;
184
+ (globalThis as any)[globalKey] = butlerManager;
185
+
186
+ butlerManager.initialize({ db, io, schema, agentManager, sessionManager }).catch(err => {
187
+ log.error({ err }, '[Butler] Background initialization failed');
188
+ });
166
189
 
167
190
  // Disconnect cleanup timers - keyed by attemptId
168
191
  const disconnectTimers = new Map<string, NodeJS.Timeout>();
@@ -408,25 +431,33 @@ app.prepare().then(async () => {
408
431
 
409
432
  // Update task status to in_progress if it was todo
410
433
  // Also clear pendingFileIds since they've been processed
411
- const taskUpdates: any = { updatedAt: Date.now() };
412
- if (task.status === 'todo') taskUpdates.status = 'in_progress';
413
- if (task.pendingFileIds) taskUpdates.pendingFileIds = null;
414
- if (Object.keys(taskUpdates).length > 1) {
415
- await db
416
- .update(schema.tasks)
417
- .set(taskUpdates)
418
- .where(eq(schema.tasks.id, taskId));
434
+ if (task.status === 'todo' || task.pendingFileIds) {
435
+ const taskUpdates: any = {};
436
+ if (task.status === 'todo') taskUpdates.status = 'in_progress';
437
+ if (task.pendingFileIds) taskUpdates.pendingFileIds = null;
438
+ await taskService.update(taskId, taskUpdates);
419
439
  }
420
440
 
421
441
  // Join attempt room
422
442
  socket.join(`attempt:${attemptId}`);
423
443
 
444
+ // Inject Butler persona into prompt when task belongs to Butler's project
445
+ let effectivePrompt = prompt;
446
+ const butlerProjectId = butlerManager.getProjectId();
447
+ if (butlerProjectId && task.projectId === butlerProjectId) {
448
+ const personaPrefix = butlerManager.buildPersonaPrompt();
449
+ if (personaPrefix) {
450
+ effectivePrompt = `${personaPrefix}\n\n---\n\n${prompt}`;
451
+ log.info({ taskId }, '[Butler] Persona context injected into task prompt');
452
+ }
453
+ }
454
+
424
455
  // Start Claude Agent SDK query
425
456
 
426
457
  agentManager.start({
427
458
  attemptId,
428
459
  projectPath: project.path,
429
- prompt,
460
+ prompt: effectivePrompt,
430
461
  model: model || undefined,
431
462
  provider: provider || undefined,
432
463
  sessionOptions: Object.keys(sessionOptions).length > 0 ? sessionOptions : undefined,
@@ -448,6 +479,11 @@ app.prepare().then(async () => {
448
479
 
449
480
  // Global event for all clients to track running tasks
450
481
  io.emit('task:started', { taskId });
482
+
483
+ // Forward to butler event collector
484
+ if (butlerManager.isEnabled()) {
485
+ butlerManager.pushEvent({ type: 'task:started', payload: { taskId }, timestamp: Date.now() });
486
+ }
451
487
  } catch (error) {
452
488
  log.error({ error }, 'Error starting attempt');
453
489
  socket.emit('error', {
@@ -896,7 +932,7 @@ app.prepare().then(async () => {
896
932
  if (!['off', 'autonomous', 'ask'].includes(mode)) return;
897
933
  log.info({ projectId, mode }, '[Autopilot] Setting mode');
898
934
  await autopilotManager.setMode(projectId, mode as AutopilotMode, {
899
- db, io, schema, agentManager, sessionManager,
935
+ db, io, schema, agentManager, sessionManager, taskService,
900
936
  });
901
937
  });
902
938
 
@@ -904,7 +940,7 @@ app.prepare().then(async () => {
904
940
  socket.on('autopilot:enable', async (data: { projectId: string }) => {
905
941
  log.info({ projectId: data.projectId }, '[Autopilot] Enable (compat → autonomous)');
906
942
  await autopilotManager.setMode(data.projectId, 'autonomous', {
907
- db, io, schema, agentManager, sessionManager,
943
+ db, io, schema, agentManager, sessionManager, taskService,
908
944
  });
909
945
  });
910
946
 
@@ -912,7 +948,7 @@ app.prepare().then(async () => {
912
948
  socket.on('autopilot:disable', async (data: { projectId: string }) => {
913
949
  log.info({ projectId: data.projectId }, '[Autopilot] Disable (compat → off)');
914
950
  await autopilotManager.setMode(data.projectId, 'off', {
915
- db, io, schema, agentManager, sessionManager,
951
+ db, io, schema, agentManager, sessionManager, taskService,
916
952
  });
917
953
  });
918
954
 
@@ -924,6 +960,99 @@ app.prepare().then(async () => {
924
960
  });
925
961
  });
926
962
 
963
+ // Butler handlers
964
+ socket.on('butler:status-request', () => {
965
+ socket.emit('butler:status', butlerManager.getStatus());
966
+ // Replay buffered notifications to late-connecting clients
967
+ const notifService = butlerManager.getNotificationService();
968
+ if (notifService) {
969
+ const buffered = notifService.getRecentNotifications();
970
+ for (const n of buffered) {
971
+ socket.emit('butler:notification', n);
972
+ }
973
+ }
974
+ });
975
+
976
+ socket.on('butler:clear-notifications', () => {
977
+ const notifService = butlerManager.getNotificationService();
978
+ if (notifService) notifService.clearBuffer();
979
+ });
980
+
981
+ socket.on('butler:enable', async () => {
982
+ log.info('[Butler] Enable requested');
983
+ await butlerManager.start();
984
+ });
985
+
986
+ socket.on('butler:disable', async () => {
987
+ log.info('[Butler] Disable requested by user');
988
+ await butlerManager.stop(true);
989
+ });
990
+
991
+ // Butler scheduler handlers
992
+ socket.on('butler:schedule-create', async (data: { cronExpression: string; actionType: string; actionPayload: any }) => {
993
+ try {
994
+ const scheduler = butlerManager.getSchedulerService();
995
+ if (!scheduler) {
996
+ socket.emit('butler:error', { message: 'Scheduler not available' });
997
+ return;
998
+ }
999
+ const task = await scheduler.createTask(data.cronExpression, data.actionType as any, data.actionPayload);
1000
+ socket.emit('butler:schedule-created', task);
1001
+ } catch (err) {
1002
+ log.error({ err }, '[Butler] Failed to create scheduled task');
1003
+ socket.emit('butler:error', { message: 'Failed to create scheduled task' });
1004
+ }
1005
+ });
1006
+
1007
+ socket.on('butler:schedule-list', async () => {
1008
+ try {
1009
+ const scheduler = butlerManager.getSchedulerService();
1010
+ if (!scheduler) {
1011
+ socket.emit('butler:schedule-list', []);
1012
+ return;
1013
+ }
1014
+ const tasks = scheduler.listTasks();
1015
+ socket.emit('butler:schedule-list', tasks);
1016
+ } catch (err) {
1017
+ log.error({ err }, '[Butler] Failed to list scheduled tasks');
1018
+ socket.emit('butler:error', { message: 'Failed to list scheduled tasks' });
1019
+ }
1020
+ });
1021
+
1022
+ socket.on('butler:schedule-delete', async (data: { id: string }) => {
1023
+ try {
1024
+ const scheduler = butlerManager.getSchedulerService();
1025
+ if (!scheduler) {
1026
+ socket.emit('butler:error', { message: 'Scheduler not available' });
1027
+ return;
1028
+ }
1029
+ const deleted = await scheduler.deleteTask(data.id);
1030
+ socket.emit('butler:schedule-deleted', { id: data.id, success: deleted });
1031
+ } catch (err) {
1032
+ log.error({ err }, '[Butler] Failed to delete scheduled task');
1033
+ socket.emit('butler:error', { message: 'Failed to delete scheduled task' });
1034
+ }
1035
+ });
1036
+
1037
+ socket.on('butler:schedule-update', async (data: { id: string; updates: any }) => {
1038
+ try {
1039
+ const scheduler = butlerManager.getSchedulerService();
1040
+ if (!scheduler) {
1041
+ socket.emit('butler:error', { message: 'Scheduler not available' });
1042
+ return;
1043
+ }
1044
+ const task = await scheduler.updateTask(data.id, data.updates);
1045
+ if (!task) {
1046
+ socket.emit('butler:error', { message: 'Task not found' });
1047
+ return;
1048
+ }
1049
+ socket.emit('butler:schedule-updated', task);
1050
+ } catch (err) {
1051
+ log.error({ err }, '[Butler] Failed to update scheduled task');
1052
+ socket.emit('butler:error', { message: 'Failed to update scheduled task' });
1053
+ }
1054
+ });
1055
+
927
1056
  socket.on('disconnect', () => {
928
1057
  log.info(`Client disconnected: ${socket.id}`);
929
1058
 
@@ -1574,6 +1703,25 @@ app.prepare().then(async () => {
1574
1703
  // Global event for all clients to track completed tasks
1575
1704
  if (attempt?.taskId) {
1576
1705
  io.emit('task:finished', { taskId: attempt.taskId, status });
1706
+
1707
+ // Forward to butler event collector (include projectId for per-project rule matching)
1708
+ if (butlerManager.isEnabled()) {
1709
+ const taskRow = await db.query.tasks.findFirst({ where: eq(schema.tasks.id, attempt.taskId) });
1710
+ const projectRow = taskRow?.projectId
1711
+ ? await db.query.projects.findFirst({ where: eq(schema.projects.id, taskRow.projectId) })
1712
+ : undefined;
1713
+ butlerManager.pushEvent({
1714
+ type: 'task:finished',
1715
+ payload: {
1716
+ taskId: attempt.taskId,
1717
+ status,
1718
+ projectId: taskRow?.projectId,
1719
+ taskTitle: taskRow?.title,
1720
+ projectName: projectRow?.name,
1721
+ },
1722
+ timestamp: Date.now(),
1723
+ });
1724
+ }
1577
1725
  }
1578
1726
 
1579
1727
  const shouldCompact = !!(status === 'completed' && usageStats?.contextHealth?.shouldCompact);
@@ -1941,6 +2089,10 @@ app.prepare().then(async () => {
1941
2089
  minioPushQueueWorker.stop();
1942
2090
  log.info('> MinIO push queue worker stopped');
1943
2091
 
2092
+ // Stop butler agent
2093
+ await butlerManager.stop();
2094
+ log.info('> Butler agent stopped');
2095
+
1944
2096
  // Cancel all Claude agents first
1945
2097
  agentManager.cancelAll();
1946
2098
  log.info('> Cancelled all Claude agents');
@@ -12,6 +12,7 @@ import { AccessAnywhereWizard } from '@/components/access-anywhere';
12
12
  import { TerminalPanel } from '@/components/terminal/terminal-panel';
13
13
  import { useProjectStore } from '@/stores/project-store';
14
14
  import { useTaskStore } from '@/stores/task-store';
15
+ import { useButlerStore } from '@/stores/butler-store';
15
16
  import { useFloatingWindowsStore } from '@/stores/floating-windows-store';
16
17
  import { useTunnelStore } from '@/stores/tunnel-store';
17
18
  import { useAgentFactoryUIStore } from '@/stores/agent-factory-ui-store';
@@ -73,6 +74,8 @@ function KanbanApp() {
73
74
 
74
75
  const { projects, selectedProjectIds, loading: projectLoading, error: projectError } = useProjectStore();
75
76
  const { selectedTask, setSelectedTask, setPendingAutoStartTask, setSelectedTaskId } = useTaskStore();
77
+ const butlerEnabled = useButlerStore(s => s.enabled);
78
+ const butlerProjectId = useButlerStore(s => s.projectId);
76
79
 
77
80
  const autoShowSetup = !projectLoading && !projectError && projects.length === 0;
78
81
 
@@ -88,9 +91,25 @@ function KanbanApp() {
88
91
  // Fetch tasks when project selection changes
89
92
  useEffect(() => {
90
93
  if (!projectLoading) {
91
- useTaskStore.getState().fetchTasks(selectedProjectIds);
94
+ let projectIds = selectedProjectIds;
95
+ // Auto-include butler project when butler is enabled and user has selected specific projects
96
+ // Skip when all-projects mode (empty array) — butler tasks already included in that mode
97
+ if (butlerEnabled && butlerProjectId && projectIds.length > 0 && !projectIds.includes(butlerProjectId)) {
98
+ projectIds = [...projectIds, butlerProjectId];
99
+ }
100
+ useTaskStore.getState().fetchTasks(projectIds);
101
+ }
102
+ }, [selectedProjectIds, projectLoading, butlerEnabled, butlerProjectId]);
103
+
104
+ // Re-fetch when butler status changes (handles delayed socket connection)
105
+ // This fixes timing issue where butler:status arrives after initial render
106
+ useEffect(() => {
107
+ if (!projectLoading && butlerEnabled && butlerProjectId && selectedProjectIds.length > 0) {
108
+ if (!selectedProjectIds.includes(butlerProjectId)) {
109
+ useTaskStore.getState().fetchTasks([...selectedProjectIds, butlerProjectId]);
110
+ }
92
111
  }
93
- }, [selectedProjectIds, projectLoading]);
112
+ }, [butlerEnabled, butlerProjectId, projectLoading, selectedProjectIds]);
94
113
 
95
114
  // Mobile: redirect panel selection to floating window
96
115
  useEffect(() => {