AIエージェント運用管理|Google Apps Script 自動生成版

/**
 * AIエージェント運用管理|Google Apps Script 自動生成版
 * --------------------------------------------------------
 * 使い方
 * 1. Googleスプレッドシートを開く
 * 2. 拡張機能 > Apps Script
 * 3. このコードを貼り付けて保存
 * 4. setupAIManagementSystem() を1回実行
 *    または、シート再読込後に「AI運用管理」メニューから実行
 *
 * standalone Apps Script から新規ファイルを作る場合:
 * createNewAIManagementSpreadsheet()
 *
 * 注意:
 * - 同名シートが既にある場合、初期構築時に削除して再作成します。
 * - 本テンプレートはAPI呼び出しを前提としません。Codexが管理表を読み書きする運用です。
 */

const AI_MGMT = {
  title: 'AIエージェント運用管理',
  locale: 'ja_JP',
  timeZone: 'Asia/Tokyo',
  sheets: ['ダッシュボード', '案件管理', 'AI社員', 'タスク', '活動ログ', '成果物', '運用マニュアル', '設定'],
  colors: {
    text: '#3F3D54',
    subText: '#726D84',
    purple: '#EDE8F9',
    purpleDark: '#66598C',
    yellow: '#FFF2B7',
    green: '#D1EFD6',
    greenDeep: '#B5D6BC',
    blue: '#C6EAF7',
    pink: '#FFDBE8',
    mauve: '#F4E8F2',
    peach: '#FFD6B7',
    lightYellow: '#FFF2BC',
    navy: '#3A446B',
    grayPurple: '#C6C1D3',
    white: '#FFFFFF',
    grid: '#E4E1EA',
    input: '#FFF9DB',
    red: '#F4CCCC'
  }
};

function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('AI運用管理')
    .addItem('初期構築(サンプルあり)', 'setupAIManagementSystem')
    .addItem('初期構築(空テンプレート)', 'setupAIManagementSystemBlank')
    .addSeparator()
    .addItem('サンプルデータを削除', 'clearSampleData')
    .addItem('ダッシュボードを再計算', 'refreshDashboard')
    .addItem('運用マニュアルを開く', 'openOperationManual')
    .addToUi();
}

/** 現在のスプレッドシートにサンプル付きで構築 */
function setupAIManagementSystem() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  if (!ss) throw new Error('アクティブなGoogleスプレッドシートが見つかりません。');
  buildAIManagementSystem_(ss, true);
  SpreadsheetApp.getUi().alert('AIエージェント運用管理を構築しました。');
}

/** 現在のスプレッドシートに空テンプレートとして構築 */
function setupAIManagementSystemBlank() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  if (!ss) throw new Error('アクティブなGoogleスプレッドシートが見つかりません。');
  buildAIManagementSystem_(ss, false);
  SpreadsheetApp.getUi().alert('空テンプレートを構築しました。');
}

/** standalone Apps Script から新規作成する場合 */
function createNewAIManagementSpreadsheet() {
  const ss = SpreadsheetApp.create(AI_MGMT.title);
  buildAIManagementSystem_(ss, true);
  Logger.log('作成完了: ' + ss.getUrl());
  return ss.getUrl();
}

function buildAIManagementSystem_(ss, includeSampleData) {
  const c = AI_MGMT.colors;
  ss.setSpreadsheetLocale(AI_MGMT.locale);
  ss.setSpreadsheetTimeZone(AI_MGMT.timeZone);

  // 同名シートを再構築。最低1枚は残す必要があるため、一時シートを使う。
  const tempName = '__BUILD_TEMP__';
  let temp = ss.getSheetByName(tempName);
  if (!temp) temp = ss.insertSheet(tempName);

  AI_MGMT.sheets.forEach(name => {
    const sh = ss.getSheetByName(name);
    if (sh) ss.deleteSheet(sh);
  });

  const dashboard = ss.insertSheet('ダッシュボード');
  const projects = ss.insertSheet('案件管理');
  const agents = ss.insertSheet('AI社員');
  const tasks = ss.insertSheet('タスク');
  const logs = ss.insertSheet('活動ログ');
  const outputs = ss.insertSheet('成果物');
  const manual = ss.insertSheet('運用マニュアル');
  const settings = ss.insertSheet('設定');

  buildProjects_(projects, includeSampleData);
  buildAgents_(agents, includeSampleData);
  buildTasks_(tasks, includeSampleData);
  buildLogs_(logs, includeSampleData);
  buildOutputs_(outputs, includeSampleData);
  buildOperationManual_(manual);
  buildSettings_(settings);
  buildDashboard_(dashboard);

  // タブ色
  dashboard.setTabColor(c.purpleDark);
  projects.setTabColor(c.yellow);
  agents.setTabColor(c.greenDeep);
  tasks.setTabColor(c.blue);
  logs.setTabColor(c.grayPurple);
  outputs.setTabColor(c.pink);
  manual.setTabColor(c.purpleDark);
  settings.setTabColor(c.navy);

  // 先頭をダッシュボードに
  ss.setActiveSheet(dashboard);
  ss.moveActiveSheet(1);

  if (ss.getSheets().length > 1 && ss.getSheetByName(tempName)) {
    ss.deleteSheet(temp);
  }

  SpreadsheetApp.flush();
}

function buildProjects_(sh, sample) {
  const c = AI_MGMT.colors;
  prepareDataSheet_(sh, 'AIエージェント案件管理', '案件の進捗と担当AIを確認します。入力欄は淡い黄色です。', 14);
  const headers = ['案件ID','案件名','依頼日','優先度','ステータス','現在工程','担当AI','進捗率','最終更新','備考','プロジェクトゴール','運用指標','値','補足'];
  sh.getRange(4,1,1,headers.length).setValues([headers]);
  styleHeader_(sh.getRange(4,1,1,11));
  sh.getRange('L4:N4').setBackground(c.purpleDark).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');

  if (sample) {
    sh.getRange('A5:K6').setValues([
      ['P-0001','玉川村アウトドア観光サイト',new Date(2026,8,7),'高','進行中','UX設計','レン',0.35,new Date(2026,8,7,10,30),'サンプル案件','自然豊かな玉川村の魅力を伝え、観光客にイベント参加と来訪を促すランディングページを完成させる'],
      ['P-0002','新規案件をここに入力','', '中','未着手','要件整理','ミカ',0,'','','']
    ]);
  }

  // KPI
  sh.getRange('L5:N10').setValues([
    ['進行中','','ステータスから集計'],
    ['完了','','ステータスから集計'],
    ['平均進捗','','案件の平均'],
    ['総案件数','','IDの件数'],
    ['今日のログ','','活動ログから集計'],
    ['稼働AI','','AI社員から集計']
  ]);
  sh.getRange('M5').setFormula('=COUNTIF(E5:E250,"進行中")');
  sh.getRange('M6').setFormula('=COUNTIF(E5:E250,"完了")');
  sh.getRange('M7').setFormula('=IFERROR(AVERAGE(H5:H250),"")');
  sh.getRange('M8').setFormula('=COUNTA(A5:A250)');
  sh.getRange('M9').setFormula('=COUNTIF(\'活動ログ\'!B5:B600,">="&TODAY())');
  sh.getRange('M10').setFormula('=COUNTIF(\'AI社員\'!D5:D50,"稼働中")');
  sh.getRange('L5:N10').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID);
  sh.getRange('L5:L10').setFontWeight('bold');

  // 入力エリア
  sh.getRange('A5:K250').setBackground(c.input);
  if (sample) sh.getRange('A5:K6').setBackground(c.lightYellow);
  sh.getRange('L5:N10').setBackground(c.white);
  sh.getRange('H5:H250').setNumberFormat('0%');
  sh.getRange('C5:C250').setNumberFormat('yyyy-mm-dd');
  sh.getRange('I5:I250').setNumberFormat('yyyy-mm-dd hh:mm');
  sh.getRange('K5:K250').setWrap(true);

  setListValidation_(sh.getRange('D5:D250'), ['緊急','高','中','低']);
  setListValidation_(sh.getRange('E5:E250'), ['未着手','進行中','承認待ち','完了','保留','エラー']);
  setListValidation_(sh.getRange('F5:F250'), ['要件整理','UX設計','デザイン','実装','品質検査','納品']);
  setNumberValidation_(sh.getRange('H5:H250'), 0, 1);

  // ステータスの条件付き書式
  addTextRules_(sh, 'E5:E250', {
    '進行中': c.blue,
    '完了': c.green,
    '承認待ち': c.yellow,
    'エラー': c.red,
    '保留': c.grayPurple
  });

  sh.setFrozenRows(4);
  setWidths_(sh, [90,230,110,110,110,110,110,100,125,120,360,110,100,160]);
  sh.setRowHeight(4,30);
}

function buildAgents_(sh, sample) {
  const c = AI_MGMT.colors;
  prepareDataSheet_(sh, 'AI社員名簿', '役割、稼働状況、利用モデルを管理します。', 8);
  const headers = ['AI ID','名前','役割','状態','担当案件','最終稼働','完了タスク','モデル'];
  sh.getRange(4,1,1,8).setValues([headers]);
  styleHeader_(sh.getRange(4,1,1,8));

  if (sample) {
    sh.getRange('A5:H9').setValues([
      ['A-01','ミカ','ディレクター','稼働中','P-0001',new Date(2026,8,7,10,30),12,'gpt-5.6-sol'],
      ['A-02','レン','UXプランナー','稼働中','P-0001',new Date(2026,8,7,10,31),9,'gpt-5.6-terra'],
      ['A-03','アオイ','UIデザイナー','待機中','',new Date(2026,8,7,9,45),15,'gpt-5.6-terra'],
      ['A-04','カイ','エンジニア','待機中','',new Date(2026,8,7,9,20),18,'gpt-5.6-sol'],
      ['A-05','ユイ','品質管理・QA','待機中','',new Date(2026,8,7,9,10),21,'gpt-5.6-luna']
    ]);
  }

  sh.getRange('A5:H50').setBackground(c.input);
  if (sample) sh.getRange('A5:H9').setBackground(c.white);
  sh.getRange('F5:F50').setNumberFormat('yyyy-mm-dd hh:mm');
  setListValidation_(sh.getRange('D5:D50'), ['待機中','稼働中','確認待ち','停止中','エラー']);
  addTextRules_(sh, 'D5:D50', {
    '稼働中': c.green,
    '待機中': c.grayPurple,
    '確認待ち': c.yellow,
    '停止中': c.peach,
    'エラー': c.red
  });

  sh.setFrozenRows(4);
  setWidths_(sh, [115,115,145,115,115,145,115,145]);
  sh.setRowHeight(4,30);
}

function buildTasks_(sh, sample) {
  const c = AI_MGMT.colors;
  prepareDataSheet_(sh, 'タスクキュー', 'エージェント間の引き継ぎと実行結果を追跡します。', 11);
  const headers = ['タスクID','案件ID','タスク名','担当AI','ステータス','優先度','作成日時','開始日時','完了日時','成果物・結果','再試行回数'];
  sh.getRange(4,1,1,11).setValues([headers]);
  styleHeader_(sh.getRange(4,1,1,11));

  if (sample) {
    sh.getRange('A5:K7').setValues([
      ['T-0001','P-0001','依頼内容を要件へ分解','ミカ','完了','高',new Date(2026,8,7,10,0),new Date(2026,8,7,10,2),new Date(2026,8,7,10,12),'要件定義を作成',0],
      ['T-0002','P-0001','ページ構成と導線を設計','レン','実行中','高',new Date(2026,8,7,10,12),new Date(2026,8,7,10,15),'','',0],
      ['T-0003','P-0001','デザイン仕様を作成','アオイ','待機','中',new Date(2026,8,7,10,12),'','','',0]
    ]);
  }

  sh.getRange('A5:K350').setBackground(c.input);
  if (sample) sh.getRange('A5:K7').setBackground(c.white);
  sh.getRange('G5:I350').setNumberFormat('yyyy-mm-dd hh:mm');
  setListValidation_(sh.getRange('E5:E350'), ['待機','実行中','確認待ち','完了','失敗','取消']);
  setListValidation_(sh.getRange('F5:F350'), ['緊急','高','中','低']);
  addTextRules_(sh, 'E5:E350', {
    '実行中': c.blue,
    '完了': c.green,
    '確認待ち': c.yellow,
    '失敗': c.red,
    '取消': c.grayPurple
  });

  sh.setFrozenRows(4);
  setWidths_(sh, [120,120,230,120,120,120,140,140,140,230,120]);
  sh.setRowHeight(4,30);
}

function buildLogs_(sh, sample) {
  const c = AI_MGMT.colors;
  prepareDataSheet_(sh, '活動ログ', 'Codexが作業の開始・進捗・引き継ぎ・完了・エラーを追記する監査ログです。', 8);
  const headers = ['ログID','日時','案件ID','タスクID','AI社員','イベント','概要','処理秒数'];
  sh.getRange(4,1,1,8).setValues([headers]);
  styleHeader_(sh.getRange(4,1,1,8));

  if (sample) {
    sh.getRange('A5:H8').setValues([
      ['L-0001',new Date(2026,8,7,10,0,0),'P-0001','T-0001','ミカ','開始','案件を受領しました',2],
      ['L-0002',new Date(2026,8,7,10,12,0),'P-0001','T-0001','ミカ','完了','要件定義を作成しました',600],
      ['L-0003',new Date(2026,8,7,10,13,0),'P-0001','T-0002','ミカ','引き継ぎ','レンへUX設計を依頼しました',3],
      ['L-0004',new Date(2026,8,7,10,15,0),'P-0001','T-0002','レン','開始','サイト構成の作成を開始しました',1]
    ]);
  }

  sh.getRange('B5:B600').setNumberFormat('yyyy-mm-dd hh:mm:ss');
  setListValidation_(sh.getRange('F5:F600'), ['受付','開始','ツール実行','引き継ぎ','完了','警告','エラー','再試行']);
  addTextRules_(sh, 'F5:F600', {
    '開始': c.blue,
    '完了': c.green,
    '引き継ぎ': c.mauve,
    '警告': c.yellow,
    'エラー': c.red,
    '再試行': c.peach
  });

  sh.setFrozenRows(4);
  setWidths_(sh, [115,145,115,115,115,115,300,115]);
  sh.setRowHeight(4,30);
}

function buildOutputs_(sh, sample) {
  const c = AI_MGMT.colors;
  prepareDataSheet_(sh, '成果物管理', '生成物の保存先、品質、承認状況を管理します。', 9);
  const headers = ['成果物ID','案件ID','種類','名称','Google Driveリンク','QAスコア','承認','状態','更新日時'];
  sh.getRange(4,1,1,9).setValues([headers]);
  styleHeader_(sh.getRange(4,1,1,9));

  if (sample) {
    sh.getRange('A5:I6').setValues([
      ['D-0001','P-0001','要件定義','要件定義書','',94,'未確認','作成済み',new Date(2026,8,7,10,12)],
      ['D-0002','P-0001','Webサイト','ランディングページ','', '', '未確認','作成中',new Date(2026,8,7,10,30)]
    ]);
  }

  sh.getRange('A5:I250').setBackground(c.input);
  if (sample) sh.getRange('A5:I6').setBackground(c.white);
  sh.getRange('I5:I250').setNumberFormat('yyyy-mm-dd hh:mm');
  setListValidation_(sh.getRange('C5:C250'), ['要件定義','構成案','デザイン','Webサイト','QAレポート','その他']);
  setListValidation_(sh.getRange('G5:G250'), ['未確認','承認','差し戻し']);
  setListValidation_(sh.getRange('H5:H250'), ['作成中','作成済み','納品済み','エラー']);
  setNumberValidation_(sh.getRange('F5:F250'), 0, 100);
  addTextRules_(sh, 'H5:H250', {
    '作成中': c.blue,
    '作成済み': c.green,
    '納品済み': c.greenDeep,
    'エラー': c.red
  });

  sh.setFrozenRows(4);
  setWidths_(sh, [115,115,115,210,260,115,115,115,145]);
  sh.setRowHeight(4,30);
}

function buildSettings_(sh) {
  const c = AI_MGMT.colors;
  prepareDataSheet_(sh, 'システム設定', 'Codex運用時の共通ルールです。APIキーやモデル呼び出し設定は使用しません。', 3);
  sh.getRange('A4:C4').setValues([['設定キー','値','説明']]);
  styleHeader_(sh.getRange('A4:C4'));
  sh.getRange('A5:C10').setValues([
    ['OPERATION_MODE','CODEX','Codexがシートを読み書きする運用'],
    ['REQUIRE_TASK_ID',true,'活動ログは原則としてタスクIDと紐づける'],
    ['REQUIRE_PROJECT_ID',true,'活動ログは原則として案件IDと紐づける'],
    ['REQUIRE_APPROVAL',true,'重要成果物・納品前は人間確認を挟む'],
    ['LOG_ON_STATUS_CHANGE',true,'開始・完了・失敗・引継ぎ時は必ずログを残す'],
    ['LOG_RETENTION_DAYS',90,'活動ログの標準保持日数']
  ]);
  sh.getRange('B5:B10').setBackground(c.input);
  sh.getRange('A5:A10').setFontWeight('bold');
  sh.setFrozenRows(4);
  setWidths_(sh, [210,180,380]);
  sh.setRowHeight(4,30);
}

function buildOperationManual_(sh) {
  const c = AI_MGMT.colors;
  sh.clear();
  sh.setHiddenGridlines(true);
  sh.getRange('A1:H80').setFontFamily('Arial').setFontColor(c.text).setVerticalAlignment('top');
  sh.setColumnWidth(1, 42);
  sh.setColumnWidth(2, 190);
  sh.setColumnWidth(3, 260);
  sh.setColumnWidth(4, 250);
  sh.setColumnWidth(5, 180);
  sh.setColumnWidth(6, 180);
  sh.setColumnWidth(7, 180);
  sh.setColumnWidth(8, 180);

  mergeAndSet_(sh, 'A1:H2', 'Codex運用マニュアル|AIエージェント運用管理');
  sh.getRange('A1:H2').setBackground(c.purple).setFontSize(18).setFontWeight('bold').setVerticalAlignment('middle');
  mergeAndSet_(sh, 'A3:H3', 'このスプレッドシートは、APIでAIを呼び出す仕組みではなく、Codexが案件・タスクを確認し、作業結果を各管理タブへ直接記録する運用を前提とします。');
  sh.getRange('A3:H3').setFontSize(10).setFontColor(c.subText).setWrap(true);

  section_(sh, 5, '1. 基本原則');
  manualRows_(sh, 6, [
    ['①','案件から開始','Codexは作業開始前に「案件管理」で案件ID、目的、現在工程、プロジェクトゴールを確認する。'],
    ['②','タスク単位で実行','実作業は「タスク」のタスクIDを単位として扱い、開始時にステータスを「実行中」へ更新する。'],
    ['③','ログを必ず残す','開始・重要な判断・引き継ぎ・完了・警告・エラー・再試行のタイミングで「活動ログ」へ1行追記する。'],
    ['④','成果物を登録','ファイル、文書、コード、調査結果など再利用可能な成果ができたら「成果物」へ登録する。'],
    ['⑤','勝手に完了扱いにしない','人間確認が必要なタスクは「確認待ち」とし、承認前に「完了」「納品済み」へ変更しない。']
  ]);

  section_(sh, 13, '2. Codexの標準実行フロー');
  sh.getRange('A14:H14').setValues([['STEP','操作','確認するタブ','更新するタブ','必須ログ','完了条件','','']]);
  sh.getRange('A14:F14').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
  sh.getRange('A15:F21').setValues([
    ['1','対象案件を特定','案件管理','-','受付','案件ID・ゴール・優先度を把握'],
    ['2','対象タスクを特定','タスク','タスク','開始','ステータス=実行中、開始日時を記録'],
    ['3','作業を実行','案件管理/タスク','必要に応じ各タブ','ツール実行','作業途中の重要判断を記録'],
    ['4','成果を保存','-','成果物','ツール実行','成果物ID・名称・リンク等を登録'],
    ['5','品質確認','成果物/タスク','成果物/タスク','警告 or 完了','問題なければ完了、要確認なら確認待ち'],
    ['6','次担当へ渡す','タスク/AI社員','タスク/AI社員','引き継ぎ','次担当・次タスクが明確'],
    ['7','終了処理','タスク','タスク/案件管理','完了','完了日時・結果・進捗を更新']
  ]);
  sh.getRange('A14:F21').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);

  section_(sh, 23, '3. 「活動ログ」への書き込みルール');
  sh.getRange('A24:H24').setValues([['列','項目','Codexが書く内容','例','','','','']]);
  sh.getRange('A24:D24').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
  sh.getRange('A25:D32').setValues([
    ['A','ログID','既存末尾+1。L-0001形式で重複させない','L-0042'],
    ['B','日時','書き込んだ時点の日本時間','2026-09-09 11:15:20'],
    ['C','案件ID','案件管理の案件ID','P-0001'],
    ['D','タスクID','タスク表のタスクID','T-0007'],
    ['E','AI社員','作業主体。Codex運用では担当AI名またはCodex','カイ / Codex'],
    ['F','イベント','受付・開始・ツール実行・引き継ぎ・完了・警告・エラー・再試行','完了'],
    ['G','概要','何をしたか、結果、次に必要なことを1~3文で具体化','資料構成を修正。第3章の重複を削除し成果物D-0005を更新。'],
    ['H','処理秒数','分かる場合のみ。分からなければ空欄でもよい','85']
  ]);
  sh.getRange('A24:D32').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);

  section_(sh, 34, '4. Codexに渡す共通指示文');
  mergeAndSet_(sh, 'A35:H42',
    'あなたはこのGoogleスプレッドシートを作業台帳として使用してください。作業開始前に「案件管理」と「タスク」を確認し、対象の案件ID・タスクID・プロジェクトゴールを把握してください。作業開始時は対象タスクを「実行中」に変更し開始日時を記録してください。作業中は重要な判断、ツール実行、引き継ぎ、警告、エラー、再試行を「活動ログ」に追記してください。作業完了時は「タスク」のステータス、完了日時、成果物・結果を更新し、再利用可能な成果物がある場合は「成果物」に登録してください。人間の確認が必要な事項は勝手に完了扱いにせず「確認待ち」としてください。既存データを削除・上書きする場合は必要性を確認し、履歴が追える状態を保ってください。'
  );
  sh.getRange('A35:H42').setBackground(c.lightYellow).setWrap(true).setFontSize(10).setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);

  section_(sh, 44, '5. ステータス更新の基準');
  sh.getRange('A45:D45').setValues([['対象','変更前','変更後','条件']]);
  sh.getRange('A45:D45').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
  sh.getRange('A46:D52').setValues([
    ['タスク','待機','実行中','Codexが実作業を開始した時'],
    ['タスク','実行中','確認待ち','人間の判断・承認が必要な時'],
    ['タスク','実行中','完了','要求された成果が揃い、追加確認が不要な時'],
    ['タスク','実行中','失敗','解消できないエラーで処理継続不能な時'],
    ['AI社員','待機中','稼働中','担当タスクの実行開始時'],
    ['AI社員','稼働中','確認待ち','人間確認待ちになった時'],
    ['AI社員','稼働中','待機中','担当タスク終了後、次タスクがない時']
  ]);
  sh.getRange('A45:D52').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);

  section_(sh, 54, '6. 禁止事項');
  manualRows_(sh, 55, [
    ['×','ログを残さず更新','案件・タスクの状態だけ変更し、活動ログを残さない運用は禁止。'],
    ['×','IDの作り直し','既存の案件ID・タスクID・成果物ID・ログIDを別用途に再利用しない。'],
    ['×','過去ログの書換え','誤記訂正を除き、過去の活動ログは原則上書きしない。訂正も新規ログで理由を残す。'],
    ['×','承認の代行','人間の承認が設定されている成果物をCodex判断だけで承認済みにしない。'],
    ['×','機密情報の記録','パスワード、APIキー、秘密鍵、個人の認証情報をセルや活動ログへ書かない。']
  ]);

  section_(sh, 62, '7. 運用開始時チェック');
  sh.getRange('A63:C63').setValues([['確認','チェック項目','基準']]);
  sh.getRange('A63:C63').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
  sh.getRange('A64:C69').setValues([
    ['□','案件IDがある','案件管理に対象案件が登録済み'],
    ['□','プロジェクトゴールがある','Codexが目的を判断できる具体度'],
    ['□','タスクIDがある','実作業単位にタスクが分解されている'],
    ['□','担当AIが決まっている','AI社員名またはCodexを明記'],
    ['□','成果物の置き場所が決まっている','Drive、Docs、コード等の保存先が明確'],
    ['□','承認要否が明確','人間確認が必要なポイントを事前に把握']
  ]);
  sh.getRange('A63:C69').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);

  sh.setFrozenRows(3);
  for (let r=1; r<=70; r++) sh.setRowHeight(r, 24);
  sh.setRowHeight(1, 34);
  sh.setRowHeight(2, 34);
  for (let r=35; r<=42; r++) sh.setRowHeight(r, 32);
}

function section_(sh, row, title) {
  const c = AI_MGMT.colors;
  sh.getRange(row,1,1,8).merge().setValue(title).setBackground(c.purpleDark).setFontColor(c.white).setFontWeight('bold').setFontSize(11).setVerticalAlignment('middle');
}

function manualRows_(sh, startRow, rows) {
  const c = AI_MGMT.colors;
  const data = rows.map(r => [r[0], r[1], r[2], '', '', '', '', '']);
  sh.getRange(startRow,1,data.length,8).setValues(data).setWrap(true).setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID);
  sh.getRange(startRow,1,data.length,1).setHorizontalAlignment('center').setFontWeight('bold');
  sh.getRange(startRow,2,data.length,1).setFontWeight('bold');
}

function buildDashboard_(sh) {
  const c = AI_MGMT.colors;
  sh.clear();
  sh.setHiddenGridlines(true);
  sh.setFrozenRows(3);
  sh.getRange('A1:V36').setFontFamily('Arial').setFontColor(c.text).setVerticalAlignment('middle');
  for (let col = 1; col <= 22; col++) sh.setColumnWidth(col, 44);

  // タイトル
  mergeAndSet_(sh, 'A1:V2', 'ピコピコ制作所|AIエージェント稼働ダッシュボード');
  sh.getRange('A1:V2').setBackground(c.purple).setFontSize(18).setFontWeight('bold').setHorizontalAlignment('left');
  mergeAndSet_(sh, 'A3:V3', 'AI社員の状態・作業・タスクをリアルタイム表示');
  sh.getRange('A3:V3').setFontSize(9).setFontColor(c.subText);

  // KPI cards
  dashboardKpi_(sh, 'A5:D5','A6:D8','進行中の案件','=COUNTIF(\'案件管理\'!E5:E250,"進行中")',c.yellow);
  dashboardKpi_(sh, 'F5:I5','F6:I8','活動中のAI社員','=COUNTIF(\'AI社員\'!D5:D50,"稼働中")',c.green);
  dashboardKpi_(sh, 'K5:N5','K6:N8','完了タスク','=COUNTIF(\'タスク\'!E5:E350,"完了")',c.blue);
  dashboardKpi_(sh, 'P5:S5','P6:S8','エラー','=COUNTIF(\'タスク\'!E5:E350,"失敗")+COUNTIF(\'AI社員\'!D5:D50,"エラー")',c.pink);

  mergeAndSet_(sh, 'A9:V10', '🏢 AI制作フロア  ● 表示あり=活動中  空席=待機・完了');
  sh.getRange('A9:V10').setBackground(c.mauve).setFontSize(11).setFontWeight('bold');

  sh.getRange('A11').setFormula('=IFERROR("🎯 現在のプロジェクトゴール|"&INDEX(FILTER(\'案件管理\'!K5:K250,\'案件管理\'!E5:E250="進行中"),1),"進行中のプロジェクトはありません")');
  sh.getRange('A11:V12').merge().setBackground(c.yellow).setFontSize(12).setFontWeight('bold').setWrap(true);

  // AI社員カード 5名
  agentCard_(sh, 5, 'B13:F14','B15:F17','B18:F18','C19:E20','👩‍💼', c.peach);
  agentCard_(sh, 6, 'I13:M14','I15:M17','I18:M18','J19:L20','🧑‍🏫', c.blue);
  agentCard_(sh, 7, 'P13:T14','P15:T17','P18:T18','Q19:S20','👩‍🎨', c.purple);
  agentCard_(sh, 8, 'F24:J25','F26:J28','F29:J29','G30:I31','👨‍💻', c.green);
  agentCard_(sh, 9, 'M24:Q25','M26:Q28','M29:Q29','N30:P31','👩‍🔬', c.pink);

  // 最新ログ
  sh.getRange('A35').setFormula('="最新ログ "&IFERROR(INDEX(\'活動ログ\'!B5:B600,COUNTA(\'活動ログ\'!A5:A600))&" "&INDEX(\'活動ログ\'!E5:E600,COUNTA(\'活動ログ\'!A5:A600))&": "&INDEX(\'活動ログ\'!G5:G600,COUNTA(\'活動ログ\'!A5:A600)),"まだログはありません")');
  sh.getRange('A35:V36').merge().setBackground(c.grayPurple).setFontColor(c.white).setFontSize(10).setWrap(true);

  // 行高
  [1,2].forEach(r => sh.setRowHeight(r,34));
  sh.setRowHeight(3,24);
  for (let r = 4; r <= 36; r++) sh.setRowHeight(r,24);
  [11,12].forEach(r => sh.setRowHeight(r,34));
  [15,16,17,26,27,28].forEach(r => sh.setRowHeight(r,31));
}

function dashboardKpi_(sh, labelRange, valueRange, label, formula, bg) {
  const c = AI_MGMT.colors;
  mergeAndSet_(sh, labelRange, label);
  sh.getRange(labelRange).setBackground(bg).setFontWeight('bold').setFontSize(9).setHorizontalAlignment('center')
    .setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
  const topLeft = valueRange.split(':')[0];
  sh.getRange(topLeft).setFormula(formula);
  sh.getRange(valueRange).merge().setBackground(c.white).setFontSize(22).setFontWeight('bold').setHorizontalAlignment('center')
    .setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
}

function agentCard_(sh, sourceRow, bubbleRange, iconRange, nameRange, stateRange, emoji, bg) {
  const c = AI_MGMT.colors;
  const bubbleCell = bubbleRange.split(':')[0];
  const iconCell = iconRange.split(':')[0];
  const nameCell = nameRange.split(':')[0];
  const stateCell = stateRange.split(':')[0];

  sh.getRange(bubbleCell).setFormula(
    '=IF(\'AI社員\'!D' + sourceRow + '="稼働中","💬 "&IFERROR(INDEX(FILTER(\'タスク\'!C$5:C$350,\'タスク\'!D$5:D$350=\'AI社員\'!B' + sourceRow + ',\'タスク\'!E$5:E$350="実行中"),1),"作業内容を確認中"),"")'
  );
  sh.getRange(bubbleRange).merge().setBackground(c.white).setFontSize(9).setWrap(true)
    .setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);

  sh.getRange(iconCell).setFormula('=IF(\'AI社員\'!D' + sourceRow + '="稼働中","' + emoji + '","")');
  sh.getRange(iconRange).merge().setBackground(bg).setFontSize(26).setHorizontalAlignment('center')
    .setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);

  sh.getRange(nameCell).setFormula('=\'AI社員\'!B' + sourceRow + '&"|"&\'AI社員\'!C' + sourceRow);
  sh.getRange(nameRange).merge().setBackground(c.grayPurple).setFontColor(c.white).setFontWeight('bold').setFontSize(9).setHorizontalAlignment('center');

  sh.getRange(stateCell).setFormula('=IF(\'AI社員\'!D' + sourceRow + '="稼働中","● 活動中","▣ 空席")');
  sh.getRange(stateRange).merge().setBackground(c.white).setFontWeight('bold').setFontSize(8).setHorizontalAlignment('center')
    .setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
}

function prepareDataSheet_(sh, title, subtitle, colCount) {
  const c = AI_MGMT.colors;
  sh.clear();
  sh.setHiddenGridlines(true);
  sh.getRange(1,1,Math.max(sh.getMaxRows(), 50),colCount).setFontFamily('Arial').setFontColor(c.text).setVerticalAlignment('middle');
  sh.getRange(1,1).setValue(title).setFontSize(16).setFontWeight('bold');
  sh.getRange(2,1).setValue(subtitle).setFontSize(9).setFontColor(c.subText);
  sh.getRange(1,1,1,colCount).merge();
  sh.getRange(2,1,1,colCount).merge();
  sh.setRowHeight(1,30);
  sh.setRowHeight(2,24);
}

function styleHeader_(range) {
  const c = AI_MGMT.colors;
  range.setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center').setWrap(true);
}

function mergeAndSet_(sh, a1, value) {
  sh.getRange(a1).merge();
  sh.getRange(a1.split(':')[0]).setValue(value);
}

function setWidths_(sh, widths) {
  widths.forEach((w, i) => sh.setColumnWidth(i + 1, w));
}

function setListValidation_(range, values) {
  const rule = SpreadsheetApp.newDataValidation()
    .requireValueInList(values, true)
    .setAllowInvalid(false)
    .build();
  range.setDataValidation(rule);
}

function setNumberValidation_(range, min, max) {
  const rule = SpreadsheetApp.newDataValidation()
    .requireNumberBetween(min, max)
    .setAllowInvalid(false)
    .build();
  range.setDataValidation(rule);
}

function addTextRules_(sh, a1, mapping) {
  const range = sh.getRange(a1);
  const rules = sh.getConditionalFormatRules();
  Object.keys(mapping).forEach(text => {
    rules.push(
      SpreadsheetApp.newConditionalFormatRule()
        .whenTextEqualTo(text)
        .setBackground(mapping[text])
        .setRanges([range])
        .build()
    );
  });
  sh.setConditionalFormatRules(rules);
}

/**
 * 活動ログへ1件追記する共通関数
 * 使用例:
 * writeActivityLog('P-0001', 'T-0002', 'レン', '開始', 'UX設計を開始しました', 1.2);
 */
function writeActivityLog(projectId, taskId, agentName, eventName, summary, processingSec) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('活動ログ');
  if (!sh) throw new Error('「活動ログ」シートがありません。先に初期構築を実行してください。');

  const nextRow = Math.max(5, sh.getLastRow() + 1);
  const logId = 'L-' + String(nextRow - 4).padStart(4, '0');
  sh.getRange(nextRow, 1, 1, 8).setValues([[
    logId,
    new Date(),
    projectId || '',
    taskId || '',
    agentName || '',
    eventName || '',
    summary || '',
    Number(processingSec || 0)
  ]]);
  sh.getRange(nextRow, 2).setNumberFormat('yyyy-mm-dd hh:mm:ss');
  return logId;
}

/** 新規案件を1件追加 */
function addProject(projectName, priority, goal) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('案件管理');
  if (!sh) throw new Error('「案件管理」シートがありません。');
  const row = nextEmptyRow_(sh, 1, 5, 250);
  const id = 'P-' + String(row - 4).padStart(4, '0');
  sh.getRange(row,1,1,11).setValues([[
    id, projectName || '', new Date(), priority || '中', '未着手', '要件整理', '', 0, new Date(), '', goal || ''
  ]]);
  sh.getRange(row,3).setNumberFormat('yyyy-mm-dd');
  sh.getRange(row,8).setNumberFormat('0%');
  sh.getRange(row,9).setNumberFormat('yyyy-mm-dd hh:mm');
  return id;
}

/** 新規タスクを1件追加 */
function addTask(projectId, taskName, agentName, priority) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('タスク');
  if (!sh) throw new Error('「タスク」シートがありません。');
  const row = nextEmptyRow_(sh, 1, 5, 350);
  const id = 'T-' + String(row - 4).padStart(4, '0');
  sh.getRange(row,1,1,11).setValues([[
    id, projectId || '', taskName || '', agentName || '', '待機', priority || '中', new Date(), '', '', '', 0
  ]]);
  sh.getRange(row,7,1,3).setNumberFormat('yyyy-mm-dd hh:mm');
  return id;
}

/** 新規成果物を1件追加 */
function addDeliverable(projectId, type, name, driveUrl) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('成果物');
  if (!sh) throw new Error('「成果物」シートがありません。');
  const row = nextEmptyRow_(sh, 1, 5, 250);
  const id = 'D-' + String(row - 4).padStart(4, '0');
  sh.getRange(row,1,1,9).setValues([[
    id, projectId || '', type || 'その他', name || '', driveUrl || '', '', '未確認', '作成中', new Date()
  ]]);
  sh.getRange(row,9).setNumberFormat('yyyy-mm-dd hh:mm');
  return id;
}

function nextEmptyRow_(sh, keyCol, startRow, maxRow) {
  const vals = sh.getRange(startRow, keyCol, maxRow - startRow + 1, 1).getValues();
  for (let i = 0; i < vals.length; i++) {
    if (!vals[i][0]) return startRow + i;
  }
  throw new Error('入力可能な行が上限に達しました。');
}

/** サンプル行のみ削除し、運用用の空テンプレートにする */
function clearSampleData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const targets = [
    ['案件管理','A5:K250'],
    ['AI社員','A5:H50'],
    ['タスク','A5:K350'],
    ['活動ログ','A5:H600'],
    ['成果物','A5:I250']
  ];
  targets.forEach(([name, a1]) => {
    const sh = ss.getSheetByName(name);
    if (sh) sh.getRange(a1).clearContent();
  });
  SpreadsheetApp.flush();
}

function refreshDashboard() {
  SpreadsheetApp.flush();
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('ダッシュボード');
  if (sh) ss.setActiveSheet(sh);
}

function openOperationManual() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('運用マニュアル');
  if (!sh) throw new Error('「運用マニュアル」シートがありません。初期構築を実行してください。');
  ss.setActiveSheet(sh);
}