# Orchestrator

> Dispatch board: chat with the orchestrating agent, watch which devices are online, and follow the tasks it starts.

| | |
|---|---|
| Version | 1.0.0 |
| Category | AI & Agents |
| Depends on | ai-chat |

# Orchestrator

Ban dieu khien giao viec: chat voi agent dieu phoi, xem thiet bi online va task dang chay — trong MOT khung.

**Type:** `ui + main` — see `.claude/skills/kazuo-create-plugin/references/plugin-examples.md`

Plugin giữ nguyên phần UI tổng hợp ai-chat / device-manager / task-manager và bổ sung backend lưu task bền vững. Backend hiện có 4 lát cắt: (1) state machine + SQLite store + IPC, (2) resource lease (`resource_leases`) + cổng năng lực `withForegroundInput` + reconcile khi khởi động + drain có thời hạn khi tắt, (3) sổ cái tác động ngoài (`side_effect_ledger`) để khởi động lại không làm lại commit/push/tin nhắn đã xảy ra, (4) chọn provider/model (mặc định chung + ghi đè theo task, từ chối chứ không tự thay thế). Sau đó bổ sung tiếp: giao việc thật (`executor-adapter.ts`), `attest()` (kiểm chứng OS process), bằng chứng kiểm chứng (`recipe_runs`), Telegram + phê duyệt có ràng buộc nội dung, Git broker, chỉ mục tài liệu, cách ly output, và **vòng tự giao việc** (`runSchedulerOnce` — việc đã duyệt tự chạy khi môi trường rảnh; mặc định TẮT). Chi tiết kiến trúc: `.claude/features/orchestrator-plugin.md`.

## Files

- `manifest.json` — metadata, `mainEntry: "main/index.js"`, `defaultSettings` (`{ agentId: '', cwd: '' }`), `defaultWidgetConfig`
  (860x620), `dependencies: ["ai-chat"]`.
- `main/` — state machine, SQLite task/event store, 10 migration (`tasks`/`task_events`, `resource_leases`,
  `side_effect_ledger`, run target, `attempts`, `recipe_runs`, `approvals`+Telegram, lời worker, `documents`
  +`quarantined_artifacts`, lý do trên sự kiện `task_events.note`), lease manager (`acquireLease`/`renewLease`/`releaseLease`/`withForegroundInput`),
  sổ cái tác động ngoài (`beginSideEffect`/`settleSideEffect`/`getSideEffect`), `git-broker.ts` (worktree riêng cho từng task ở **mỗi** repo của môi trường, danh sách lệnh git được phép, cổng kiểm read-only tách riêng (`checkIntegrable`), merge có duyệt, không push, và xoá worktree theo thứ tự lồng-trước-ngoài-sau (`removeWorktrees`)),
  `approvals.ts` + `telegram.ts` (phê duyệt có ràng buộc nội dung, Telegram **khoá mặc định**),
  `token-store.ts` (**token bot được mã hoá bằng `safeStorage`/DPAPI, KHÔNG nằm trong settings** — không mã
  hoá được thì không lưu; giao diện chỉ thấy trạng thái, không bao giờ thấy giá trị),
  `verify-runner.ts` + bảng `recipe_runs` (coordinator tự chạy lệnh kiểm chứng của môi trường và quan sát mã thoát),
  `environment-identity.ts` + `attest()` (coordinator tự hỏi OS xem Unity Editor có còn là cái đã claim không, fail closed),
  `document-index.ts` (chỉ **gắn cờ** tài liệu trùng tên khác nội dung / lâu không đổi — không bao giờ sửa gì),
  `artifact-cleanup.ts` (**cách ly, không có lệnh xoá nào**; chứng minh bằng danh tính tệp `bigint` rằng đã
  chuyển đúng tệp; một lượt quét mỗi thư mục),
  `executor-adapter.ts` (giao việc thật:
  chỉ task `ready`, môi trường theo id từ settings, lease trên thư mục, không tự đổi provider/model), reconcile khi
  `activate()` và drain có thời hạn khi `deactivate()`, IPC theo hằng `CHANNELS` được export, và 9 công cụ MCP cho
  điều phối viên (`list_environments`/`create_task`/`list_tasks`/`task_detail`/`dispatch_task`/
  `list_unity_dialogs`/`click_unity_dialog`/`scan_documents`/`preview_cleanup` — **không có công cụ nào duyệt được phạm vi, và không công cụ nào chuyển
  được tệp**: dọn thật và khôi phục chỉ gọi được từ widget).
- `test/` — 418 test cho state machine, transaction/rollback/crash/lifecycle/IPC, lease/generation/reconcile/drain,
  sổ cái tác động ngoài, bảng `attempts`, giao việc thật, vòng tự giao việc, kho token (mã hoá tại chỗ),
  truy vết, chỉ mục tài liệu, cách ly/khôi phục output và packaging; chạy bằng `npm test`.
- `widget.html` — nạp chat embed + Golden Layout của ai-chat qua `app://./plugins/ai-chat/...`;
  layout: head (title + select agent + ↻ + ＋ chat mới + ⊞ khôi phục bố cục + ⚙) → `#dockRoot` + `#dockStaging`
  (5 vùng Thiết bị / Task / Hàng việc / Chat / Tài liệu & Dọn dẹp, dock được, kéo thả + đổi kích thước,
  bố cục tự lưu). Panel Tài liệu & Dọn dẹp chung ngăn với Chat vì nó liệt kê đường dẫn đầy đủ — thêm
  một vùng làm bố cục đã lưu cũ bị dựng lại **một lần** (xem `isValidDockLayout`).
- `js/widget.js` — `loadRoles()`, `mountChat(agentId)` (KazuoChatEmbed), `loadDevices()`, `loadTasks()`,
  `loadJobs()` + panel Hàng việc (tạo việc, duyệt, giao — mỗi nút là một sự kiện chuyển trạng thái có thật),
  panel Tài liệu & Dọn dẹp (`runDocsScan`/`runDocsPreview`/`docsRun`/`loadQuarantine`/`restoreQuarantined`)
  — **không** nằm trong `refreshAll()`/poll vì quét và xem trước đều đi hết thư mục trên đĩa; nút dọn chỉ
  bật khi đang có bản xem trước của đúng môi trường, và phải bấm hai lần với số tệp + dung lượng hiện
  trên nhãn nút,
  `refreshAll()` + poll 5s, refresh ngay khi agent kết thúc lượt (`chat.on('turn-end')`).
- `css/widget.css` — layout 2 cột (side 250px + chat), chấm online, viền trái task theo status.
- `locales/en.json5` / `locales/vi.json5` — UI strings, namespace `widget.*` / `status.*`.

`settings.html` + `js/settings.js` chỉ có cwd + nhịp làm mới; agent điều phối, provider và model mặc định
đều chọn ngay trong widget (dropdown + ⚙) và lưu vào settings của plugin.

## Default settings

- `agentId` — default: `''` (rỗng → tự chọn agent khớp `manager|nova|orchestr`, không có thì agent đầu)
- `cwd` — default: `''` (rỗng → thư mục mặc định của phiên chat)
- `defaultProviderId` — default: `''` — hồ sơ môi trường ai-chat dùng chung cho mọi task chưa tự chọn
- `defaultModel` — default: `''` — model dùng chung cho mọi task chưa tự chọn
- `unityProjectRoots` — default: `[]` — thư mục gốc các môi trường Unity được phép bấm hộp thoại.
  **Rỗng = không cho phép gì**, tính năng tắt.
- `docStaleAfterDays` — default: `90` — bao lâu không đổi nội dung thì tài liệu bị gắn cờ "lâu không xem lại".
  Đây là **giá trị khởi điểm để người dùng chỉnh**, không phải con số đo được từ ba repo này.
- `outputExpireAfterDays` — default: `30` — output cũ hơn bấy nhiêu ngày thì được coi là hết hạn. Cũng là giá
  trị khởi điểm, chưa đo.
- `preflightRamPerEnvironmentGb` — default: `8` — RAM cần cho mỗi môi trường sắp khởi (đo thật lúc rảnh:
  5,3–5,7 GB/môi trường, cộng dư cho lúc Unity nạp/biên dịch).
- `preflightRamReserveGb` — default: `16` — RAM luôn chừa cho Windows và app khác.
- `preflightMinFreeDiskGb` — default: `20` — ổ chứa mỗi môi trường phải còn trống ít nhất bấy nhiêu. Giá trị
  khởi điểm, **chưa đo**.

Ba ngưỡng trên dùng cho công cụ `check_capacity` (xem feature doc): hỏi TRƯỚC khi mở Unity/game, thiếu thì
từ chối **tất cả** môi trường được hỏi.

Mỗi môi trường trong `environments` có thêm ba trường, **cả ba đều trống mặc định**:

- `docs` — danh sách đường dẫn (tương đối với `cwd`) mà chỉ mục tài liệu được phép nhìn. Rỗng = không quét gì.
  Là danh sách chọn lọc chứ không phải quét cả dự án: thứ không khai ở đây thì **không thể** thành phát hiện.
- `outputRoot` — thư mục chứa output của môi trường, nơi **duy nhất** lệnh dọn được phép chuyển tệp. Rỗng =
  không dọn được. **Không bao giờ suy ra từ `cwd`** — suy ra là chĩa một lượt chuyển tệp vào cả dự án ngay lần
  đầu có người thử. **Phải là đường dẫn tuyệt đối**, khác `docs` ở ngay trên: `docs` được giải quyết tương đối
  với `cwd`, còn `outputRoot` đi thẳng vào `path.resolve()` nên đường dẫn tương đối sẽ trỏ theo thư mục làm
  việc của Electron. Cũng không có gì bắt nó phải nằm trong `cwd`.
- `nestedRepo` — `{ path, integrationBranch }`: repo git **thứ hai** nằm bên trong `cwd` mà repo ngoài
  gitignore chứ không theo dõi (ví dụ: repo dự án gitignore thư mục `engine/`, còn `engine/` là
  clone của một repo khác). `path` tương đối với `cwd`, được **resolve rồi mới kiểm** nên `..` không thoát ra
  ngoài được. Khai sai làm **cả môi trường bị bỏ** — chạy với một nửa Git còn tệ hơn không chạy. Một object,
  không phải mảng: có đúng một repo lồng.

- `game` — `{ exe, args, profile }`: game của môi trường cho `login_game` / nút "Đăng nhập game". `exe`
  (vd `Bin/Game.exe`) và `profile` (vd `tools/profiles/dev01.json`) tương đối với `cwd` và phải
  nằm trong `cwd`. Game chưa chạy thì được mở (sau `check_capacity`), rồi đăng nhập bằng script tự động
  của dự án. Không khai = môi trường đó không đăng nhập game được.

- `unity` — `{ exe, project, mcpUrl }`: Unity Editor cho `restart_unity` / nút "Khởi động lại Unity". `exe`
  tuyệt đối tới `Unity.exe`, `project` (vd `engine`) tương đối và nằm trong `cwd`, `mcpUrl` là
  `http://localhost:<cổng>/`. Từ chối (không đóng gì) khi có task đang chạy, scene chưa lưu, hoặc Unity Hub tắt.

Bật `integrationBranch` + `nestedRepo` cho ba môi trường thử nghiệm tốn khoảng **1,1 GB đĩa mỗi task**
(≈ 780 MB repo ngoài + ≈ 350 MB repo lồng); worktree dùng chung kho object nên **không** copy `.git`
(610 MB + 6,3 GB). Submodule `StreamingAssets` **không** được init (7,3 GB mỗi task), nên worktree **không mở
được bằng Unity Editor như một project hoàn chỉnh** — nó là nơi sửa mã, không phải nơi build.

## Kênh IPC sử dụng (của plugin khác, đều mở)

- `plugin:ai-chat:list-agents` — danh sách agent-role để chọn agent điều phối
- `plugin:device-manager:fleet-devices` — thiết bị của tài khoản + trạng thái online
- `plugin:task-manager:list` — task (agent-role tự log mỗi lần `dispatch_agent`)

Thiếu device-manager/task-manager → 2 pane hiện dòng gợi ý bật plugin, phần chat vẫn dùng bình thường.

## Test

```bash
cd plugins-library/orchestrator
npm test
```

Kết quả nghiệm thu hiện tại: 126 test pass. Từ gốc repo, chạy thêm `npx tsc -p tsconfig.plugins.json --noEmit`.

Không reload/toggle `ai-chat` để kiểm tra backend Orchestrator. Chỉ triển khai runtime sau khi đã đồng bộ plugin theo quy trình host và có quyền bảo trì phù hợp.
