SLOPSHOPPER

ai-maestro-board

Shows an agent its own AI Maestro task board inside the terminal: a count in the status line and a /board command that lists the cards by column, with what…

newcommandstatusprocesstimer
★ 835v0.1.0MITupdated 2026-10-2023blocks-OS/ai-maestro/mods/ai-maestro-board
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ai-maestro-board
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /board ⎿ ai-maestro-board: I do not know which agent this is. Inside AI Maestro it is set for you; elsewhere set the "Agent id" option of ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

<img src="./docs/logo-constellation.svg" alt="AI Maestro Logo" width="120"/>

AI Maestro

I was running 35 AI agents across multiple terminals and became the human mailman between them. So I built AI Maestro.

The OS for AI-first organizations — orchestrate any AI agent with persistent memory, agent-to-agent messaging, and multi-machine support.

Version Platform-lightgrey) License GitHub Stars

AI Maestro Dashboard

Quick Start · Features · Skills · Protocols · Documentation · Contributing


The Story

I gave an AI agent a real task — not autocomplete, a real engineering problem. It checked the code, read the logs, queried the database, and came back with the answer. That was the moment. This thing can actually work.

Within a week I was running 35 agents across terminals. They were productive, but they couldn't talk to each other. I became the human message bus — copying context from one terminal, pasting into another. I was the bottleneck in my own AI team.

So I built AI Maestro — one dashboard to see every agent, on every machine, with persistent memory and direct agent-to-agent communication. Today I run 80+ agents across multiple computers, building real companies with them every day.

What makes this different:

  • Works with the agents you already use — Claude Code, Codex and Grok Build get launch, chat history, status and approval cards. Pi, OpenClaw and GitHub Copilot CLI launch from AI Maestro and run in the terminal tab, as does any other terminal agent. What each one supports. We don't lock you in.
  • Multi-machine from day one — Peer mesh network with no central server. Nobody else does this.
  • Agents that communicate — The Agent Messaging Protocol (AMP) lets agents coordinate directly. You orchestrate, they collaborate.
  • Yours, entirely — MIT licensed. No account, no telemetry, no per-seat pricing, no hosted tier we upsell you to. Your code and your agents stay on your machines.

See it

<img src="./docs/images/home.png" alt="The AI Maestro dashboard: six agents with role, company and live status (Working, Needs you, Ready), and the header pages" width="900"/>

One dashboard for every agent: who is working, who needs you, who is ready. The header opens the extra pages (Teams, Org, Matrix, Attention, Zoom).

<a href="./docs/images/zoom-wall.jpg"><img src="./docs/images/zoom-wall.jpg" alt="Zoom view: every agent as a card with photo, status, role and tags" width="440"/></a><br/>Zoom. Every agent at a glance, with photo, status, role and tags.<a href="./docs/images/agent-profile.jpg"><img src="./docs/images/agent-profile.jpg" alt="Agent profile with identity, program, model, tags and commits" width="440"/></a><br/>Agent profile. Name, role, picture, program, model, tags and the commits an agent made.
<a href="./docs/images/matrix.png"><img src="./docs/images/matrix.png" alt="The Matrix: open cards in four Eisenhower squares across all agents, with due badges and co-owners" width="440"/></a><br/>Matrix. Every open card in four squares, across agents and hosts. Due dates and co-owners show on the card.<a href="./docs/images/attention.png"><img src="./docs/images/attention.png" alt="The Attention page: overdue cards, cards in review, cards waiting on you and agents that need you" width="440"/></a><br/>Attention. What needs you: overdue cards, work waiting on you, agents asking a question.
<a href="./docs/images/card-detail.jpg"><img src="./docs/images/card-detail.jpg" alt="Card detail with due date, where the card came from and its owners" width="440"/></a><br/>Card detail. Due date, several owners and where the card came from.<a href="./docs/images/team-kanban.png"><img src="./docs/images/team-kanban.png" alt="A team meeting with agent tiles and a Kanban board" width="440"/></a><br/>Team meeting. A war room: agent tiles beside a shared Kanban board.
<a href="./docs/images/secrets-card.png"><img src="./docs/images/secrets-card.png" alt="A card in the chat where an agent asks for a credential, with a masked field" width="440"/></a><br/>Secrets. An agent asks for a credential; you type it into a masked field in the chat; the agent uses it by name and never sees the value.<a href="./docs/images/canvas-page.png"><img src="./docs/images/canvas-page.png" alt="A canvas page an agent generated, with a table and Approve and Hold buttons" width="440"/></a><br/>Canvas. An agent draws an interactive page in the dashboard; your click goes back to it as an action.
<a href="./docs/images/chat-cards.png"><img src="./docs/images/chat-cards.png" alt="Chat with a question card and an approval card" width="440"/></a><br/>Chat. Questions and approvals arrive as cards you answer with one click.<a href="./docs/images/commit-trace.jpg"><img src="./docs/images/commit-trace.jpg" alt="Commit to session: find which agent session made a commit" width="440"/></a><br/>Commit to session. Find which agent session made a commit.

On a phone: <a href="./docs/images/mobile-matrix.png">Matrix</a> · <a href="./docs/images/mobile-home.png">home</a> · <a href="./docs/images/mobile-board.png">board</a>.

Short videos (click to play): <img src="./docs/videos/header-pages-tour.jpg" alt="Tour of the header pages" width="210"/> <img src="./docs/videos/cli-card-appears-on-matrix.jpg" alt="A card created from the command line appears on the Matrix" width="210"/> <img src="./docs/videos/canvas-action-reaches-agent.jpg" alt="A canvas click reaches the agent" width="210"/> <img src="./docs/videos/multi-agent-kanban-card-moves.jpg" alt="Cards move across the Kanban while agents work" width="210"/> <img src="./docs/videos/secret-request-and-store.jpg" alt="An agent asks for a secret and you store it" width="210"/>

Screens are from a demo company ("Northwind Labs"); the multi-agent run uses scripted agents.


The Mental Model

AI Maestro is an operating system for an AI-first company. Not a task runner — a place where a standing team works.

An agent here is closer to an employee than to a job. It has a name, a face, a memory that survives the session, an inbox — and it owns something: a product, a repository, a process, a customer. Its memory and code graph are indexed against what it owns, which is why it gets better at that thing over months rather than starting cold every morning.

Because agents own things, they don't share a working copy. Two agents that need the same repository each clone it, work on their own branch, and integrate through git — push, pull request, review, merge — exactly like two engineers on a team.

That's deliberate, and it follows from the one thing that defines this product: your agents run on different machines. A shared checkout needs a shared filesystem. Git worktrees — the isolation primitive the single-machine agent IDEs are built on — are several working directories over one .git store on one disk, so they stop working the moment your backend agent is on a Linux box and your iOS agent is on a Mac. A clone is the only primitive that survives the move. It's why transferring an agent to another host clones its repos to the destination: an agent's repositories travel with the agent.

The full model →


Quick Start

curl -fsSL https://raw.githubusercontent.com/23blocks-OS/ai-maestro/main/scripts/remote-install.sh | bash

This installs everything you need:

  • AI Maestro dashboard and service
  • Agent messaging system (AMP)
  • The AI Maestro plugin for Claude Code: 14 skills and their CLI scripts, plus the aim-secret and safe-psql helpers

Time: 5-10 minutes · Requires: Node.js 18+, tmux, and 4 GB of RAM to build (or 2 GB plus swap; once built it runs in a few hundred MB) · Optional: Claude Code 2.1.287+ for the inbox mod

Windows: Install WSL2 first (PowerShell as Administrator, then restart), then open Ubuntu from the Start menu and run the curl command there:

wsl --install

New to WSL? WSL is a Linux system with its own disk, so its home folder starts empty. Keep agents there (~/agents/<name>, the default), not in C:\ folders, which are much slower from WSL. Open their files from Windows at \\wsl.localhost\Ubuntu\home\<you>\agents. Windows in 5 minutes · Full Windows guide

Linux: Ensure build tools are installed: sudo apt install tmux build-essential. On a cloud VM, read the Linux sizing and deployment guide: the machine to ask for (4 GB RAM, 2 vCPU, 15-25 GB disk), the memory each part uses, and running under pm2.

git clone https://github.com/23blocks-OS/ai-maestro.git
cd ai-maestro
yarn install
yarn dev

See QUICKSTART.md for detailed setup options.

Dashboard opens at http://localhost:23000

Update an existing install from its folder (pulls, updates submodules, builds, reinstalls hooks and the plugin, restarts):

./update-aimaestro.sh -y

Come back to it without remembering the address: the installer adds an ai-maestro command (ai-maestro open starts the server if it is down, then opens the dashboard; also start, stop, status) and, on a Mac, an AI Maestro app in ~/Applications you can drag to the Dock. It can also start at login. Chrome and Edge offer "Install app" from the dashboard too. Skip all of it with --skip-launcher; remove it with ./scripts/install-launcher.sh --remove.


Features

Every feature was born from running a real AI-first organization. We built them in the order we needed them.

One Dashboard

I had 35 terminals and couldn't tell which was which.

See and manage all your AI agents in one place. Create agents from the UI with a guided wizard, organize them with smart naming (project-backend-api becomes a 3-level tree with auto-coloring), and switch between any agent with a click. Four deployment modes: tmux (local), Docker (containerized), AWS EC2 (dedicated instance), and AWS ECS Fargate (serverless). Auto-discovers tmux sessions, Docker containers, cloud deployments, and standalone agents.

Any Machine

My Mac Mini was sitting there idle. What if I ran agents on that too?

A peer mesh network where every machine is equal. Add a computer, it joins the mesh. Every agent on every machine, visible from one dashboard. Use each machine for what it's best at — Mac for iOS builds, Linux for Docker, cloud for heavy compute. No central server required.

Worker machines can run headless (yarn headless) — the full API and agent runtime with no UI, in about 100MB of RAM. Run the dashboard where you sit; run agents wherever the compute is.

Agent Messaging

I was the mailman — copying messages between agents because they couldn't talk to each other.

The Agent Messaging Protocol (AMP) gives your agents email-like communication. Priority levels, message types, cryptographic signatures, and push notifications. Tell your agent "send a message to backend about the deployment" — it just works. Agents coordinate directly while you manage the big picture.

Before AMP: You copy research from one terminal, paste into another, repeat 50 times a day. With AMP: "Research agent, send your findings to the writing agent." Done.

Gateways

A friend in Singapore wanted his agents to talk to mine. But I didn't want to give him access to my network.

Connect your AI agents to Slack, Discord, Email, and WhatsApp through organizational gateways. Smart routing (@AIM:agent-name), thread-aware responses, and content security with 34 prompt injection patterns detected at the gateway — before any agent sees the message.

Persistent Memory

Every morning, my agents woke up with amnesia.

Long-term memory is a skill you switch on per agent. Every night each agent's conversations become memory: short statements of what it learned, each backed by the passages it came from, and an entity graph of the things it works with and how they relate (runs on, depends on, stores data in, deploys to), kept current, with ended relations marked. Claude Code deletes transcripts after 30 days; AI Maestro rebuilds that history from the agent's own message index, so months of work become memory instead of disappearing. When a prompt names something, the agent is told what it relates to before it acts, so it can see what a change affects. Knowledge that comes up in more sessions weighs more; secrets are redacted and never stored. How it works →

Alongside it: Code Graph (interactive visualization of your entire codebase with delta indexing) and Documentation (auto-generated, searchable docs from your code). Agents get smarter the longer they work with you.

Chat

I wanted to answer an agent's question without finding its terminal.

Talk to an agent in a chat view, on desktop or on your phone. When the agent asks a question or needs an approval, it shows up as a card you can click, and the click sends the same key you would press in the terminal. Claude Code, Codex and Grok Build all work here. How the chat works →

Secrets

I kept pasting API keys into chats, and the model kept seeing them.

Agents use a credential without seeing it. aim-secret keeps values in a local vault (macOS Keychain, Linux libsecret, or an encrypted file). An agent asks for a secret by name; a card in the chat, with a masked field, lets you store it; the agent then runs commands that use it by name, with the value scrubbed from the output. In Claude Code, the ai-maestro-secrets mod asks through a form in the terminal instead. Scrubbing stops accidents, not an agent that wants the value, so use narrowly scoped keys. Secrets → · Claude Code mods →

Work Coordination

Talking isn't working. I needed agents to coordinate on actual deliverables.

Assemble agents into teams, run meetings in split-pane war rooms, and track tasks on a full Kanban board with drag-and-drop, dependencies, and 5 status columns. Cross-machine teams work seamlessly. This is project management for your AI workforce.

Every agent also keeps its own board, and any agent, in any framework, can feed it with the agent-tasks skill. A card can carry a due date, several owners (it shows on each owner's board), a kind (task, idea, bug, research) and where it came from. Three extra pages, reached from the header and never replacing the main view, read the same cards: the Matrix (Eisenhower squares across all agents and hosts), the Org view, and Attention (agents that need you, cards waiting on you, overdue cards). Tasks, Matrix and Attention →

World view (beta) is an experiment: switch it on in Settings → Experiments and the header gets a World button that draws your hosts and organizations as 3D islands with their agents on them, in the live status colours. World view →

Claude Code Mods

A Claude Code session with no tmux has nowhere for a message to land.

Mods are plugins that run inside Claude Code 2.1.287 or later. AI Maestro ships two: an inbox mod that shows unread agent messages in the status line and above the prompt, and a secrets mod that lets an agent request a credential. AI Maestro itself works with any current Claude Code version. Details and limits →

Canvas

Sometimes the answer is a form, a chart or an approval, not a paragraph.

Agents draw interactive HTML pages in the dashboard, and what you click or submit comes back to them as a structured action (AAP). Agent actions →

Scheduled Work

Some jobs shouldn't wait for me to remember to ask.

Give an agent its own schedule — morning triage, a nightly dependency check, a Monday report. The timer belongs to the agent, not the machine, so a schedule travels with the agent when it moves hosts, and fires when that agent goes idle instead of interrupting it mid-task.

Agent Presence

At 80 agents, they all looked the same.

Custom avatars, personality profiles, and roles for every agent. When an agent has a face and a job title, you instinctively assign it the right work — just like a real team.

Agents can also speak and be seen: a voice pipeline with your choice of TTS provider, and live animated faces that move while the agent talks. Open a call with an agent and it looks back at you. Nobody else is doing this, and once you've reviewed a plan by listening to it on a walk, the terminal feels like a downgrade.

One caveat worth stating plainly: true lip-sync — mouth movement driven by the actual audio — needs the OpenAI or ElevenLabs voice and an API key. The default browser voice (web-speech) renders straight to your speakers and exposes no audio stream to measure, so the face animates from a synthetic envelope instead. It looks alive; it is not tracking the words. That is a limit of the Web Speech API, not something we plan to work around.

Agent Deployment

One agent on my laptop. Another on EC2. A third on Fargate. All in the same dashboard.

Four ways to run agents, each for a different need:

ModeWhatBest For
tmuxDirect terminal sessions on your machineLocal development, zero setup
DockerContainerized agents with resource limitsIsolation, reproducibility, multi-project
AWS EC2Dedicated Graviton instance with native install, Nginx + SSLAlways-on agents, SSH access, persistent workloads
AWS ECS FargateServerless containers, auto-built Docker imageBurst scaling, zero maintenance, pay-per-use

Cloud agents are Terraform-managed. EC2 installs Node.js, tmux, and AI CLIs directly on ARM64 hardware (no Docker overhead). ECS auto-builds your Docker image, pushes to ECR, and runs on Fargate. Both deploy with one command from the dashboard or CLI.

# EC2: dedicated instance with SSL
aimaestro-agent.sh create my-api --ec2 \
  --domain api.example.com --ssl-email admin@example.com --key-name my-key

# ECS Fargate: serverless (auto-builds image)
aimaestro-agent.sh create worker --ecs

Meet Lola — Your First Hire

Every AI-first organization starts with one agent. Lola is yours — a batteries-included Chief of Staff framework built for AI Maestro. She handles email triage, semantic memory, task management, and content security out of the box. Install the platform, deploy Lola, and you have your first employee on day one.

# Clone and deploy Lola on AI Maestro
git clone https://github.com/23blocks-OS/lolabot.git
cd lolabot && ./setup.sh

The LolaBot Ecosystem:

  • LolaBot Framework — Open-source agent framework. Build your own Lola.
  • lolabots.com — The LolaBot Factory. Pre-built agent templates, one-click deploy.

Learn more about Lola →


The Agent Ecosystem

Every agent is built from five dimensions:

| | Dimension | What | Where | |-|-----------|------|-------| | WHO | Personality | Domain expertise, workflows, deliverables | Agent Library (150+) | | HOW | Capabilities | Skills, scripts, CLI tools, Canvas | Plugin Builder | | TRUST | Identity | Cryptographic keys, OAuth tokens | AID | | TALK | Communication | Agent-to-agent messaging | AMP | | ACT | Actions | Clicks and forms from agent-drawn pages | AAP |

See Skills and Protocols below.

AI Maestro is the stage. Pick personalities, give them skills, and run them from one dashboard.

Read the full ecosystem guide →


Skills

The AI Maestro plugin (version 1.18.0) carries 14 skills in the open Agent Skills Standard format. An agent loads a skill when its description matches the task.

SkillWhat it doesSource
memory-searchSearch what the agent learned in past sessions, and what depends on an entityplugin builder
docs-searchLook up signatures and APIs in the generated docsplugin builder
graph-queryCallers, dependencies and blast radius from the code graphplugin builder
ai-maestro-agents-managementCreate, rename, hibernate, wake and delete agentsplugin builder
planningPersistent plan, findings and progress files for long tasksplugin builder
agent-tasksThe agent's own task board: quadrant, waiting, parked ideas, origin, due dates, shared cards, and import of an existing listplugin builder
aim-secret-managementUse a credential without seeing it; ask the user to store oneplugin builder
agent-messagingSigned messages between agents (AMP)agentmessaging/claude-plugin
agent-identitySign in with an Ed25519 identity and OAuth 2.0 tokens (AID)agentmessaging/agent-identity
canvas-actionsInteractive pages in the dashboard and their actions (AAP)agentmessaging/agent-actions
agent-filesShare files across hosts through an AFP spaceagentmessaging/agent-files
agentic-sqlRead-only SQL against real databases through safe-psql; changes data only through safe-psql-write, after a person approves each change23blocks-OS/agentic-sql
agentic-supportNumbered support cases with a written trail, and runbooks23blocks-OS/agentic-support
agentic-seoSEO audit and implementation for websites23blocks-OS/agentic-seo

The installer and updater set up the helpers the skills need (aim-secret, safe-psql). To add only

Source 2 files
hooks/register.tsx 246 lines
1import { atom, update } from 'claude-code'
2import type { EngineInterface as Engine, Register } from 'claude-code'
3
4// An agent's own AI Maestro board, inside its terminal session. READ-ONLY: this mod never writes a
5// card (the agent moves cards with aimaestro-task.sh, the agent-tasks skill). It matters most for
6// agents that run without the AI Maestro dashboard, e.g. a lolabot.
7//
8// Where the cards come from, in order:
9//   1. the AI Maestro server on this host: GET /api/agents/<id>/board (own cards + team cards, with the
10//      quadrant); asked through curl, so the mod needs no other tool;
11//   2. no server: ~/.aimaestro/agents/<id>/tasks.json, the file the server would have read (own cards only).
12// Honest limits: the mods API is early access; the status line may be shared with other mods; project
13// cards are not on the board yet.
14
15const POLL_MS = 30_000
16const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/ // the same rule the server uses for a folder name
17const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
18const DEFAULT_API = 'http://localhost:23000'
19
20export type Card = {
21  id: string
22  subject: string
23  status: string
24  quadrant: string | null
25  waitingOn?: string
26  waitingNote?: string
27  scope?: string
28  teamName?: string
29}
30
31export type Summary = { open: number; waitingOnMe: number } | null
32
33const summary = atom({ plugin: 'ai-maestro-board', key: 'summary' } as const, null as Summary)
34
35let cards: Card[] = []
36let source: 'server' | 'file' | null = null
37let who = ''
38
39/** No control, separator or escape characters, bounded length: card text comes from other people and agents. */
40export function clean(text: unknown, limit = 120): string {
41  const chars = Array.from(String(text ?? ''), c => (c === ' ' || !/[\p{C}\p{Z}]/u.test(c) ? c : ' '))
42  const s = chars.join('').replace(/\s+/g, ' ').trim()
43  return s.length <= limit ? s : s.slice(0, limit - 1) + '…'
44}
45
46// Everything the logic needs from the engine, so tests can hand it a world from memory.
47export type EnvName = 'HOME' | 'AIM_AGENT_ID' | 'CLAUDE_AGENT_ID' | 'AMP_DIR' | 'AIM_AGENT_NAME' | 'CLAUDE_AGENT_NAME' | 'AIMAESTRO_API'
48
49export type IO = {
50  env: (name: EnvName) => Promise<string | undefined>
51  run: (argv: string[], timeoutMs: number) => Promise<{ exitCode: number; stdout: string }>
52  read: (path: string) => Promise<string | null>
53}
54
55const ioOf = ($: Engine): IO => ({
56  // $.env.get takes a literal name, so the variables this mod reads can be listed
57  env: async name => {
58    switch (name) {
59      case 'HOME': return (await $.env.get('HOME')) ?? undefined
60      case 'AIM_AGENT_ID': return (await $.env.get('AIM_AGENT_ID')) ?? undefined
61      case 'CLAUDE_AGENT_ID': return (await $.env.get('CLAUDE_AGENT_ID')) ?? undefined
62      case 'AMP_DIR': return (await $.env.get('AMP_DIR')) ?? undefined
63      case 'AIM_AGENT_NAME': return (await $.env.get('AIM_AGENT_NAME')) ?? undefined
64      case 'CLAUDE_AGENT_NAME': return (await $.env.get('CLAUDE_AGENT_NAME')) ?? undefined
65      default: return (await $.env.get('AIMAESTRO_API')) ?? undefined
66    }
67  },
68  run: async (argv, timeoutMs) => {
69    const r = await $.process.run(argv, { timeoutMs })
70    return { exitCode: r.exitCode, stdout: r.stdout }
71  },
72  read: async path => {
73    try {
74      return String(await $.fs.read(path))
75    } catch {
76      return null
77    }
78  },
79})
80
81/** Quadrant from the two answers; null unless BOTH are given (false/false is q4, not "unplaced"). */
82export function quadrantOf(urgent: unknown, important: unknown): string | null {
83  if (typeof urgent !== 'boolean' || typeof important !== 'boolean') return null
84  return urgent ? (important ? 'q1' : 'q3') : (important ? 'q2' : 'q4')
85}
86
87/**
88 * Which agent is this? The way amp-helper.sh does it, with the plugin option first:
89 * option agentId, AIM_AGENT_ID (set in sessions AI Maestro starts), CLAUDE_AGENT_ID, the folder name in AMP_DIR,
90 * then a name (AIM_AGENT_NAME, CLAUDE_AGENT_NAME) that the server can turn into an id.
91 */
92export async function identify(io: IO, option: unknown): Promise<{ id?: string; name?: string }> {
93  const candidates: Array<string | undefined> = [
94    typeof option === 'string' && option.trim() ? option.trim() : undefined,
95    await io.env('AIM_AGENT_ID'),
96    await io.env('CLAUDE_AGENT_ID'),
97  ]
98  const amp = await io.env('AMP_DIR')
99  if (amp) candidates.push(amp.replace(/\/+$/, '').split('/').pop())
100  for (const c of candidates) if (c && ID_RE.test(c)) return { id: c }
101  for (const name of [await io.env('AIM_AGENT_NAME'), await io.env('CLAUDE_AGENT_NAME')]) {
102    if (name && ID_RE.test(name)) return { name }
103  }
104  return {}
105}
106
107function toCard(raw: any): Card | null {
108  if (!raw || typeof raw !== 'object' || typeof raw.id !== 'string' || typeof raw.subject !== 'string' || typeof raw.status !== 'string') return null
109  return {
110    id: raw.id,
111    subject: raw.subject,
112    status: raw.status,
113    quadrant: typeof raw.quadrant === 'string' ? raw.quadrant : quadrantOf(raw.urgent, raw.important),
114    waitingOn: typeof raw.waitingOn === 'string' ? raw.waitingOn : undefined,
115    waitingNote: typeof raw.waitingNote === 'string' ? raw.waitingNote : undefined,
116    scope: typeof raw.scope === 'string' ? raw.scope : undefined,
117    teamName: typeof raw.teamName === 'string' ? raw.teamName : undefined,
118  }
119}
120
121function parseCards(text: string, key: 'cards' | 'tasks'): Card[] | null {
122  try {
123    const data = JSON.parse(text)
124    const list = data?.[key]
125    if (!Array.isArray(list)) return null
126    return list.map(toCard).filter((c): c is Card => c !== null)
127  } catch {
128    return null
129  }
130}
131
132async function get(io: IO, api: string, path: string): Promise<string | null> {
133  try {
134    const r = await io.run(['curl', '-sS', '--max-time', '5', '-f', `${api}${path}`], 8000)
135    return r.exitCode === 0 ? r.stdout : null
136  } catch {
137    return null
138  }
139}
140
141/** The cards of one agent: from the server, else from the board file. null when neither answers. */
142export async function load(io: IO, option: unknown, apiOption: unknown): Promise<{ cards: Card[]; source: 'server' | 'file'; id: string } | null> {
143  const home = (await io.env('HOME')) ?? ''
144  const api = ((typeof apiOption === 'string' && apiOption.trim()) || (await io.env('AIMAESTRO_API')) || DEFAULT_API).replace(/\/+$/, '')
145  const me = await identify(io, option)
146  let id = me.id
147  if (!id && me.name) {
148    // only a name: ask the server which agent that is
149    const list = await get(io, api, '/api/agents')
150    try {
151      const agents = JSON.parse(list ?? '{}').agents as Array<{ id: string; name?: string; alias?: string }> | undefined
152      id = agents?.find(a => a.name === me.name || a.alias === me.name)?.id
153    } catch { /* no server, no id */ }
154  }
155  if (!id || !ID_RE.test(id)) return null
156
157  const body = await get(io, api, `/api/agents/${encodeURIComponent(id)}/board`)
158  const fromServer = body === null ? null : parseCards(body, 'cards')
159  if (fromServer) return { cards: fromServer, source: 'server', id }
160
161  const file = await io.read(`${home}/.aimaestro/agents/${id}/tasks.json`)
162  const fromFile = file === null ? null : parseCards(file, 'tasks')
163  return fromFile ? { cards: fromFile, source: 'file', id } : null
164}
165
166export function summarize(list: Card[]): Summary {
167  const open = list.filter(c => c.status !== 'completed' && c.status !== 'backlog')
168  if (open.length === 0) return { open: 0, waitingOnMe: 0 }
169  return { open: open.length, waitingOnMe: open.filter(c => c.status === 'waiting' && c.waitingOn === 'juan').length }
170}
171
172export function statusText(s: Summary): string | undefined {
173  if (!s || s.open === 0) return undefined
174  return `board: ${s.open} open${s.waitingOnMe > 0 ? `, ${s.waitingOnMe} waiting on you` : ''}`
175}
176
177const COLUMNS: Array<[string, string]> = [
178  ['pending', 'To Do'], ['in_progress', 'In Progress'], ['review', 'Review'], ['waiting', 'Waiting'], ['backlog', 'Backlog'],
179]
180
181function waitingLabel(c: Card): string {
182  const on = c.waitingOn
183  const base = !on ? 'waiting' : on === 'juan' ? 'waiting on you' : on === 'outside' ? 'waiting on someone outside' : on === 'hold' ? 'on hold' : `waiting on ${clean(on, 40)}`
184  return c.waitingNote ? `${base}: ${clean(c.waitingNote, 80)}` : base
185}
186
187/** The board as text, by column. Completed cards are not shown. */
188export function render(list: Card[], opts: { who: string; source: 'server' | 'file' }): string {
189  const open = list.filter(c => c.status !== 'completed')
190  const s = summarize(list)
191  const lines: string[] = [`Board of ${clean(opts.who, 60)}: ${s?.open ?? 0} open${s && s.waitingOnMe > 0 ? `, ${s.waitingOnMe} waiting on you` : ''}${opts.source === 'file' ? '  (from the board file; no AI Maestro server answered, team cards are not shown)' : ''}`]
192  if (open.length === 0) {
193    lines.push('No open cards.')
194    return lines.join('\n')
195  }
196  for (const [status, label] of COLUMNS) {
197    const col = open.filter(c => c.status === status)
198    if (col.length === 0) continue
199    lines.push('', `${label} (${col.length})`)
200    for (const c of col) {
201      const q = c.quadrant ? c.quadrant.toUpperCase() : '--'
202      const team = c.scope === 'team' && c.teamName ? `  (team: ${clean(c.teamName, 40)})` : ''
203      lines.push(`  ${clean(c.id.slice(0, 8), 8)}  ${q}  ${clean(c.subject)}${status === 'waiting' ? `  [${waitingLabel(c)}]` : ''}${team}`)
204    }
205  }
206  lines.push('', 'Q1 do first, Q2 schedule, Q3 delegate, Q4 delete, -- not placed. Move cards with aimaestro-task.sh (the agent-tasks skill).')
207  return lines.join('\n')
208}
209
210async function refresh($: Engine, options: Record<string, unknown>) {
211  const r = await load(ioOf($), options.agentId, options.apiUrl)
212  if (!r) {
213    cards = []
214    source = null
215    await update($, summary, () => null)
216    $.ui.status(undefined)
217    return
218  }
219  cards = r.cards
220  source = r.source
221  who = r.id
222  const s = summarize(cards)
223  await update($, summary, () => s)
224  $.ui.status(options.showStatus === false ? undefined : statusText(s))
225}
226
227export const register: Register = (on, options) => {
228  on('session.start', async ($, e, next) => {
229    await $.command.register({ name: 'board', description: 'Show your AI Maestro task board: cards by column, what each waits on, its quadrant' })
230    await refresh($, options as Record<string, unknown>)
231    $.clock.every(POLL_MS, () => { void refresh($, options as Record<string, unknown>) })
232    return next(e)
233  })
234
235  on('command.run', { command: 'board' }, async $ => {
236    await refresh($, options as Record<string, unknown>)
237    if (!source) {
238      const me = await identify(ioOf($), (options as Record<string, unknown>).agentId)
239      return { text: me.id || me.name
240        ? `No board found for ${clean(me.id || me.name, 60)}: the AI Maestro server did not answer and there is no board file for it.`
241        : 'I do not know which agent this is. Inside AI Maestro it is set for you; elsewhere set the "Agent id" option of this mod to the agent\'s AMP identity id.' }
242    }
243    return { text: render(cards, { who: UUID_RE.test(who) ? who.slice(0, 8) : who, source }) }
244  })
245}
246
types/index.d.ts 8 lines
1export type BoardSummary = { open: number; waitingOnMe: number } | null
2
3declare module 'claude-code' {
4  interface PluginState {
5    'ai-maestro-board': { summary: BoardSummary }
6  }
7}
8