UNPKG

claude-code-company

Version:

Multi-agent tmux coordination system for Claude Code with perfect Unicode support

247 lines (214 loc) 13 kB
const { sendCleanMessage, getCurrentRole, tmuxCommand, getCallerWindow, setCachedWindowId, clearWindowIdCache, isWindowIdCacheLocked } = require('./tmux'); const { getAgentPane } = require('./agents'); const { colors, clearWindowConfig } = require('./config'); const logger = require('./utils/logger'); const { validateNotNull, validateString } = require('./utils/validation'); const { getCurrentSession } = require('./utils/persistence'); // 基本メッセージ送信(リトライ機能付き) async function sendMessage(targetAgent, message, retryCount = 3) { let lastError; for (let attempt = 1; attempt <= retryCount; attempt++) { try { // 入力検証 validateNotNull(targetAgent, 'targetAgent'); validateString(targetAgent, 'targetAgent'); validateNotNull(message, 'message'); validateString(message, 'message'); const pane = await getAgentPane(targetAgent); if (!pane) { throw new Error(`Agent ${targetAgent} not found or not initialized`); } // 保存されたセッション情報から正しいウィンドウIDを取得 let targetWindowId = null; const session = getCurrentSession(); if (session && session.agentPaneMap && session.agentPaneMap[targetAgent]) { targetWindowId = session.agentPaneMap[targetAgent].windowId; logger.info(`Using saved window ID ${targetWindowId} for ${targetAgent}`); } await sendCleanMessage(pane, message, targetWindowId); logger.success(`Message sent to ${targetAgent}`); return; // 成功時は即座に終了 } catch (error) { lastError = error; logger.error(`Attempt ${attempt}/${retryCount} failed for ${targetAgent}: ${error.message}`); if (attempt < retryCount) { logger.warn(`Retrying in ${attempt * 1000}ms...`); await new Promise(resolve => setTimeout(resolve, attempt * 1000)); } } } // 全ての試行が失敗した場合 const finalError = new Error(`Failed to send message to ${targetAgent} after ${retryCount} attempts. Last error: ${lastError.message}`); logger.error(finalError.message); throw finalError; } // 統一された送信コマンド async function chainCommand(target, command) { const { getActiveWorkers } = require('./agents'); try { // コマンド実行開始時にwindow IDをキャッシュし、ロック const windowId = await getCallerWindow(); setCachedWindowId(windowId, true); // trueでロックを有効化 logger.info(`Window ID cached and locked: ${windowId}`); const currentRole = await getCurrentRole(); logger.phase(currentRole); // デフォルト送信先の処理 if (!target || target.trim() === '') { target = 'president'; console.log(`${colors.blue}📋 送信先未指定のため、デフォルトでpresidentに送信します${colors.nc}`); } // ターゲットに基づいて送信先を決定 if (target.toLowerCase() === 'boss1' || target.toLowerCase() === 'boss') { // President → Boss1 await sendMessage('boss1', `【President指示】${command}\n\nあなたはboss1です。\n⚠️ 重要: Boss1は実装作業を行いません。タスク分解と委任のみを行います。\nこの指示を具体的なタスクに分解してWorkersに委任してください`); logger.success(`President → Boss1 に指示を送信しました`); } else if (target.toLowerCase() === 'workers' || target.toLowerCase() === 'worker') { // Boss1からWorkersへのタスク分散 const { getActiveWorkers } = require('./agents'); logger.info(`Boss1 → Workers へタスクを分散します`); // アクティブなWorkerを動的取得 const workers = await getActiveWorkers(); if (workers.length === 0) { throw new Error('利用可能なWorkerが見つかりません。setupコマンドを実行してください'); } // Boss1へのフィードバック送信 try { await sendMessage('boss1', `【分散開始】${command}\n\nあなたはboss1です。\n⚠️ 重要: Boss1は実装作業を行いません。タスク分解と委任のみを行います。\n${workers.length}個のアクティブWorkerへタスクを送信中...`); } catch (bossError) { logger.warn(`Boss1へのフィードバック送信に失敗: ${bossError.message}`); } // 並列送信と個別エラーハンドリング const results = await Promise.allSettled( workers.map(async (worker) => { try { await sendMessage(worker, `【作業指示】${command}\n\nあなたは${worker}です。\n⚠️ 重要: 実装作業はWorkerのみが行います。あなたがコードの作成・修正を実行してください。\n実装完了後: npx claude-code-company@latest send "完了内容"`); return worker; } catch (error) { // 個別のWorkerエラーを適切に処理 logger.error(`${worker}への送信失敗: ${error.message}`); throw error; } }) ); // 結果を解析して成功/失敗を報告 const successful = results.filter(r => r.status === 'fulfilled').map(r => r.value); const failed = results.filter(r => r.status === 'rejected'); // Boss1への結果通知を安全に実行 try { if (successful.length > 0) { await sendMessage('boss1', `【分散成功】成功: ${successful.join(', ')}\n\nあなたはboss1です。\n⚠️ 重要: Boss1は実装作業を行いません。Workersの進捗管理と報告のみを行います。`); } if (failed.length > 0) { const errorMessages = failed.map((f, i) => `${workers[results.indexOf(f)]}: ${f.reason.message}`); await sendMessage('boss1', `【分散警告】失敗: ${errorMessages.join(', ')}\n\nあなたはboss1です。\n⚠️ 重要: Boss1は実装作業を行いません。Workersの進捗管理と報告のみを行います。`); } // 最終完了通知 await sendMessage('boss1', `【分散完了】タスク: "${command}"\n成功: ${successful.length}/${workers.length}個のWorker\n\nあなたはboss1です。\n⚠️ 重要: Boss1は実装作業を行いません。Workersの進捗管理と報告のみを行います。`); console.log(`${colors.green}✅ Tasks delegated to workers!${colors.nc}`); } catch (finalError) { console.log(`${colors.yellow}⚠️ Boss1への結果通知送信に失敗: ${finalError.message}${colors.nc}`); } } else if (target.toLowerCase() === 'president') { // Boss1 → President (escalate) try { await sendMessage('president', `【プロジェクト完了報告】\n${command}\n\nあなたはpresidentです。\n⚠️ 重要: Presidentは実装作業を行いません。報告の確認と承認のみを行います。`); console.log(`${colors.green}✅ Report escalated to President!${colors.nc}`); } catch (presidentError) { console.error(`${colors.red}❌ Presidentへの報告送信失敗: ${presidentError.message}${colors.nc}`); throw new Error(`Failed to report to President: ${presidentError.message}`); } } else if (target.startsWith('worker')) { // Boss1への報告として処理 const workerName = target; // 自身への確認メッセージ try { await sendMessage(workerName, `【報告送信完了】\n"${command}"\n\nあなたは${workerName}です。Boss1からの次の指示を待機中`); } catch (workerError) { console.error(`${colors.yellow}⚠️ ${workerName}への確認送信失敗: ${workerError.message}${colors.nc}`); } // Boss1への報告 try { await sendMessage('boss1', `【${workerName}完了報告】\n${command}\n\nあなたはboss1です。\n⚠️ 重要: Boss1は実装作業を行いません。進捗管理と報告のみを行います。\n他のWorkersの進捗確認後、必要に応じてPresidentに最終報告`); console.log(`${colors.green}✅ Report sent to Boss1!${colors.nc}`); } catch (bossError) { console.error(`${colors.red}❌ Boss1への報告送信失敗: ${bossError.message}${colors.nc}`); throw new Error(`Failed to report to Boss1: ${bossError.message}`); } } else { // ターゲットが不明な場合のエラー console.log(`${colors.red}❌ 不明な送信先: ${target}${colors.nc}`); console.log(`\n${colors.yellow}📋 使用可能な送信先一覧${colors.nc}`); console.log(`${colors.blue}┌─────────────┬────────────────────────────────────────┐${colors.nc}`); console.log(`${colors.blue}│ 送信先 │ 説明 │${colors.nc}`); console.log(`${colors.blue}├─────────────┼────────────────────────────────────────┤${colors.nc}`); console.log(`${colors.blue}${colors.purple}president${colors.blue} │ 統括者(プロジェクト全体を管理) │${colors.nc}`); console.log(`${colors.blue}${colors.cyan}boss1${colors.blue} │ 調整者(タスクを分解してWorkerに分散) │${colors.nc}`); console.log(`${colors.blue}${colors.green}workers${colors.blue} │ 全Worker(並列でタスクを実行) │${colors.nc}`); console.log(`${colors.blue}${colors.green}worker1-3${colors.blue} │ 個別Worker(特定のWorkerに送信) │${colors.nc}`); console.log(`${colors.blue}└─────────────┴────────────────────────────────────────┘${colors.nc}\n`); throw new Error(`Invalid target: ${target}`); } } catch (error) { console.error(`${colors.red}❌ コマンド実行エラー: ${error.message}${colors.nc}`); throw error; } finally { // コマンド終了時にキャッシュとロックをクリア clearWindowIdCache(); logger.info('Window ID cache and lock cleared'); } } // pane 0以外を削除する機能 async function destroyPanes() { try { // 開始時に一度だけwindow IDを取得して固定ロック const currentWindow = await getCallerWindow(); setCachedWindowId(currentWindow, true); // trueでロックを有効化 console.log(`${colors.purple}🔒 Window ID固定: ${currentWindow} - destroy実行中は他windowへの操作を防止${colors.nc}`); // ペイン番号一覧を取得(逆順で削除するため降順ソート) // 固定されたwindow IDでペイン一覧を取得 const panesOutput = await tmuxCommand('list-panes', '-t', currentWindow, '-F', '#{pane_index}'); const paneIndices = panesOutput.trim().split('\n') .filter(line => line.trim()) .map(index => parseInt(index)) .sort((a, b) => b - a); // 降順ソート if (paneIndices.length <= 1) { console.log(`${colors.yellow}⚠️ 削除するペインがありません (現在のペイン数: ${paneIndices.length})${colors.nc}`); return; } // pane 0以外を削除(降順なので最後から削除) let deletedCount = 0; for (const paneIndex of paneIndices) { if (paneIndex !== 0) { try { // ウィンドウ.ペイン形式で指定 const target = `${currentWindow}.${paneIndex}`; await tmuxCommand('kill-pane', '-t', target); deletedCount++; console.log(`${colors.green}✅ ペイン${paneIndex}を削除${colors.nc}`); } catch (error) { console.error(`${colors.red}❌ ペイン${paneIndex}削除失敗: ${error.message}${colors.nc}`); } } } if (deletedCount === 0) { console.log(`${colors.yellow}⚠️ 削除対象のペインがありませんでした (ペイン0のみ存在)${colors.nc}`); } else { console.log(`${colors.green}${deletedCount}個のペインを削除しました (ペイン0は保持)${colors.nc}`); // ウィンドウ設定をクリア clearWindowConfig(currentWindow); console.log(`${colors.blue}🗑️ Window ${currentWindow} の設定をクリアしました${colors.nc}`); } } catch (error) { console.error(`${colors.red}❌ ペイン削除エラー: ${error.message}${colors.nc}`); throw error; } finally { // コマンド終了時にキャッシュをクリア(ロック解除) clearWindowIdCache(); console.log(`${colors.purple}🔓 Window ID固定解除: destroy完了${colors.nc}`); } } module.exports = { sendMessage, chainCommand, destroyPanes };