claude-code-company
Version:
Multi-agent tmux coordination system for Claude Code with perfect Unicode support
247 lines (214 loc) • 13 kB
JavaScript
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
};