docs(rag): rewrite the KB docs to the real gateway verb surface

The RAG knowledge base (indexed and queried by agents at runtime)
described entire fictional MCP tool surfaces — roboco_task_*,
roboco_journal_*, roboco_message_send, roboco_notify_send, roboco_agent_*,
roboco_session_*, roboco_workspace_*, roboco_project_* — that don't exist,
so agents searching the KB were handed invented tool names.

Rewrite every affected doc (tools, roles, workflows, troubleshooting, and
the stale architecture snippets) to the real surface: the gateway intent
verbs (give_me_work, i_will_work_on, open_pr, i_am_done, claim_review,
pass, fail, claim_doc_task, i_documented, triage, delegate, i_will_plan,
unblock, complete, escalate_up, escalate_to_ceo, ...) and content tools
(commit, note(scope=...), say, dm, evidence, notify*, open_session,
channels). Also reconcile the access-control docs to code: CEO can cancel
(Board/Auditor cannot); the management-channel membership and the
Auditor's silent-but-present status now match communications.py.
This commit is contained in:
Renn F
2026-06-05 17:20:36 +02:00
parent 416356899d
commit ecea593a51
27 changed files with 999 additions and 1009 deletions
+37 -61
View File
@@ -1,82 +1,58 @@
# A2A (Agent-to-Agent) Tools
## Overview
A2A is direct peer-to-peer messaging between agents. There is **no**
`roboco_agent_*` or `roboco_a2a_*` tool — A2A is the `dm` content tool on
the `roboco-do` MCP server, with `channels()` for discovery and the
notify inbox for receiving.
A2A enables direct peer-to-peer communication between agents about existing tasks.
**Key points:**
- Direct HTTP when both agents online (no notification)
- Fallback to notification only when target offline
- All requests MUST reference an existing `task_id`
## Tools
### roboco_agent_discover
Find agents by role, team, or skill.
## Send a direct message — `dm`
```python
roboco_agent_discover(
role="developer", # Optional: developer, qa, documenter, cell_pm, etc.
team="backend", # Optional: backend, frontend, ux_ui
skill="code_review" # Optional: specific capability
dm(
recipient="be-qa", # target agent slug
text="Please review my changes",
task_id="abc123...", # auto-filled from your active task if omitted
skill=None, # optional skill slug to scope the conversation
)
```
### roboco_agent_request
- Auto-creates the conversation; auto-resolves the skill if needed.
- **Same-cell only.** Cross-cell DM is denied by policy — route through
your Cell PM via `escalate_up(task_id, reason)`.
- The recipient sees it in their notify inbox when offline.
Send A2A message to another agent.
## Discover who/where to message — `channels`
There is no agent-directory tool. Use `channels()` to see the channels
you can read/write, and post to a channel when the audience is the whole
cell rather than one peer:
```python
roboco_agent_request(
target_agent="be-qa",
skill="code_review",
message="Please review my changes",
task_id="abc123...", # REQUIRED
options={"urgent": False} # Optional: priority queue
)
channels() # -> {"writable": [...], "readable": [...]}
say(channel="backend-cell", text="Anyone hit Y before? Starting task X.")
```
**Returns:** `{status, delivery, message_id}` where `delivery` is `"direct"` or `"notification"`.
## Receive incoming messages
### roboco_a2a_check
Poll your inbox for incoming A2A messages.
Incoming A2A and @mentions land in your notify inbox. When `i_am_idle()`
soft-blocks on unread items, drain the inbox:
```python
roboco_a2a_check()
notify_list(unread_only=True) # list pending items
notify_get(notification_id) # read one (marks it read)
notify_ack(notification_id) # acknowledge after handling
```
**Returns:** `{messages: [...], count: N}` - messages from other agents.
## When to use A2A
**Note:** A hook automatically notifies you of pending messages after tool calls.
- A quick question, sanity check, or hand-off about a task you own
- Requesting code review or clarification from a same-cell peer
## Common Use Cases
## When NOT to use A2A
| Need | Action |
|------|--------|
| Code review | `roboco_agent_request("be-qa", "code_review", "...", task_id)` |
| Clarification | `roboco_agent_request("be-pm", "clarification", "...", task_id)` |
| Find reviewer | `roboco_agent_discover(skill="code_review")` |
| Urgent help | `roboco_agent_request(..., options={"urgent": True})` |
## When to Use A2A
- Communication about an existing task you're working on
- Requesting code review, clarification, or help
- Notifying another agent about task progress
- Urgent questions needing immediate attention
## When NOT to Use A2A
- Creating new work → Only PMs create tasks via `roboco_task_create`
- Task assignments → PM assigns via `roboco_task_assign`
- Escalations → Use `roboco_task_escalate`
- Formal notifications → Use `roboco_notify_send` (PM only)
## Task Creation Rules
Only Cell PMs and Main PM can create tasks (subtasks).
If an agent receives an A2A request that requires new work:
1. Escalate to PM: `roboco_task_escalate(task_id, "Needs subtask for...")`
2. PM decides whether to create a subtask
| Need | Do this instead |
|------|-----------------|
| Cross-cell question | `escalate_up(task_id, reason)` — DM is same-cell only |
| New work / subtask | Only PMs create work, via `delegate(...)`; escalate to your PM |
| Formal, ack-required signal | PM/Board `notify(target, text, ...)` |
| Cell-wide broadcast | `say(channel=..., text=...)` |
+89 -60
View File
@@ -1,92 +1,121 @@
# Journal Tools
## Creating Entries
There is **no** `roboco_journal_*` tool. Journaling is a single content
tool on the `roboco-do` MCP server: `note`. The `scope` argument selects
the entry kind; structured fields are filled per scope.
| Tool | Purpose |
|------|---------|
| `roboco_journal_entry` | General entry |
| `roboco_journal_decision` | Decision log |
| `roboco_journal_learning` | Learning capture |
| `roboco_journal_struggle` | Problem/solution |
| `roboco_journal_reflect` | Task reflection |
```python
note(
text: str, # always: one-paragraph summary
scope: str = "note", # note | decision | reflect | learning | struggle
task_id: str | None = None, # auto-filled from your active task if omitted
title: str | None = None,
# decision-scope fields:
context: str = "",
options=None, # list of {name, pros, cons} (a single dict is ok)
chosen: str = "",
rationale: str = "",
consequences=None, # list of strings (a single string is ok)
# reflect-scope fields:
what_done: str = "",
what_learned: str = "",
what_struggled: str = "",
next_steps=None, # list of strings (a single string is ok)
)
```
`text` is always required. Missing narrative fields default to a visible
placeholder rather than being rejected — the note is always recorded.
## Scopes
| Scope | Use For | Structured fields |
|-------|---------|-------------------|
| `note` | General entry | (just `text`) |
| `decision` | Decision log | `context`, `options`, `chosen`, `rationale`, `consequences` |
| `reflect` | Task reflection | `what_done`, `what_learned`, `what_struggled`, `next_steps` |
| `learning` | Learning capture | (just `text`) |
| `struggle` | Problem / blocker | (just `text`) |
## General Entry
```python
roboco_journal_entry({
type: "learning",
title: "Redis SCAN vs KEYS",
content: "SCAN is better for large datasets",
task_id: task_id,
tags: ["redis", "performance"]
})
note(
text="SCAN is better than KEYS for large datasets",
scope="learning",
title="Redis SCAN vs KEYS",
task_id=task_id,
)
```
Entry types: `task_reflection`, `decision_log`, `learning`, `struggle`, `general`
## Decision Log
```python
roboco_journal_decision({
title: "Session storage choice",
context: "Need fast session lookups",
options: ["PostgreSQL", "Redis"],
chosen: "Redis",
rationale: "Sub-ms reads, ephemeral data"
})
note(
text="Chose Redis for session storage over PostgreSQL.",
scope="decision",
title="Session storage choice",
context="Need fast session lookups",
options=[
{"name": "PostgreSQL", "pros": "durable", "cons": "slower reads"},
{"name": "Redis", "pros": "sub-ms reads", "cons": "ephemeral"},
],
chosen="Redis",
rationale="Sub-ms reads, ephemeral data",
consequences=["Session loss on Redis restart is acceptable"],
)
```
## Learning
```python
roboco_journal_learning({
content: "asyncio.gather for parallel calls",
how_applied: "Reduced latency 50%",
category: "performance",
tags: ["async"]
})
note(
text="asyncio.gather for parallel calls — reduced latency 50%",
scope="learning",
title="Parallel async calls",
)
```
## Struggle (Problem/Solution)
## Struggle (Problem / Blocker)
```python
roboco_journal_struggle({
task_id: task_id,
problem: "Tests failing intermittently",
attempts: ["Timeout increase", "Retry logic"],
resolution: "Race condition in setup"
})
note(
text=(
"Tests failing intermittently — tried timeout increase and retry "
"logic; root cause was a race condition in setup."
),
scope="struggle",
task_id=task_id,
)
```
## Reflection (Required)
## Reflection
Use a `reflect`-scope note before submitting to QA — it gives QA the
"why" behind the diff.
```python
roboco_journal_reflect({
task_id: task_id,
what_done: "Implemented rate limiting",
what_learned: "Lua scripts for atomicity",
what_struggled: "Testing concurrency"
})
note(
text="Implemented rate limiting with a Redis-backed sliding window.",
scope="reflect",
task_id=task_id,
what_done="Implemented rate limiting",
what_learned="Lua scripts give atomicity for the counter increment",
what_struggled="Testing concurrency deterministically",
next_steps=["Add a load test for the 100-req boundary"],
)
```
## Reading Journals
Journals are written by `note` and surface through the knowledge base —
there is no separate journal-read tool. Search past notes (yours and
your team's, where permitted) via the `roboco-optimal` MCP server:
```python
# Search your journal
roboco_journal_search("rate limiting", top_k=5)
# Semantic search over indexed notes/decisions/learnings
roboco_kb_search(query="rate limiting", index_types=["journals", "decisions"])
# Recent entries
roboco_journal_recent(limit=10)
# Read team journals (if permitted)
roboco_journal_read_team(
target_agent="be-dev-1",
task_id=task_id
)
# Your stats
roboco_journal_stats()
# Check access scope
roboco_journal_scope()
# Conversational lookup with follow-up context
roboco_ask_mentor(question="What did we decide about session storage?")
```
+54 -58
View File
@@ -1,84 +1,80 @@
# Messaging Tools
## Sending Messages
There is **no** `roboco_message_*`, `roboco_notify_send`, or
`roboco_session_*` tool. Messaging is a small set of **content tools** on
the `roboco-do` MCP server. They are role-scoped at spawn time.
## Channel post — `say`
```python
roboco_message_send({
channel: "backend-cell",
content: "Starting work on rate limiting",
task_id: task_id
})
say(channel="backend-cell", text="Starting work on rate limiting", task_id=task_id)
```
## Channel History
- `channel` is the slug WITHOUT a leading `#`.
- `task_id` is auto-filled from your active task if omitted.
- Write access varies by role; the gateway returns `not_authorized` and
lists the channels you *can* write to.
Don't invent channel slugs. Call `channels()` first if unsure:
```python
# Read channel history
roboco_channel_history(
channel="backend-cell",
limit=50
)
channels() # -> {"writable": [...], "readable": [...]}
```
## Notifications
Valid slugs: cell channels (`backend-cell`, `frontend-cell`,
`uxui-cell`); cross-cell (`dev-all`, `qa-all`, `pm-all`, `doc-all`);
management (`main-pm-board`, `board-private`); broadcast
(`announcements`, `all-hands`).
### Sending (PM/Board only)
## Direct message (A2A) — `dm`
```python
roboco_notify_send({
recipient: "be-dev-1",
type: "task_assignment",
task_id: task_id,
message: "Task ready for you"
})
dm(recipient="be-qa", text="Quick sanity check: ...", task_id=task_id)
```
### Receiving
- `recipient` is an agent slug (`be-pm`, `be-dev-1`, `ceo`, ...).
- Auto-creates the conversation; `task_id` auto-fills from your active task.
- Same-cell only. Cross-cell DM is denied by policy — route through your
Cell PM via `escalate_up(task_id, reason)`.
## Formal notification — `notify` (PM / Board only)
`notify` creates an ack-required notification (distinct from the informal
`say`/`dm`). Only PM roles and the Board may send it; devs / QA / docs use
`say` and `dm`.
```python
# List notifications
notifications = roboco_notify_list()
# Acknowledge
roboco_notify_ack(notification_id)
notify(target="be-dev-1", text="Task ready for you", priority="normal", task_id=task_id)
```
### Notification Types
`priority` is `normal | high | urgent`. `task_id` auto-injects from the
active task when omitted.
| Type | Purpose |
|------|---------|
| `task_assignment` | New task assigned |
| `priority_change` | Priority updated |
| `blocker_escalation` | Task blocked |
| `review_request` | Review needed |
| `documentation_request` | Docs needed |
| `alert` | General alert |
| `broadcast` | Org-wide message |
## Receiving notifications
## Sessions
Every role with an inbox gets these (so `i_am_idle()` doesn't soft-block
on unread items):
```python
# Create session for tasks
roboco_session_create_for_tasks({
title: "Feature X Implementation",
task_ids: [task_1_id, task_2_id]
})
# Start collaborative session
roboco_session_start(
channel="backend-cell",
session_type="collaborative",
task_id=task_id
)
notify_list(unread_only=True, limit=20) # your inbox
notify_get(notification_id) # read one (marks it read)
notify_ack(notification_id) # acknowledge after handling
```
## Message Types
When `i_am_idle()` reports unread A2A or @mentions, list -> get -> ack,
then idle again. (The Auditor gets `notify_list`/`notify_get` for inbox
visibility but does not ack.)
| Type | Use For |
|------|---------|
| `reasoning` | Thought process |
| `dialogue` | Discussion |
| `decision` | Decisions made |
| `action` | Actions taken |
| `blocker` | Blocking issues |
| `technical` | Technical details |
## Sessions (PM-or-up only)
Devs / QA / docs participate via channels and DMs and do **not** open
sessions. PMs and the Board link discussion threads to tasks:
```python
open_session(task_id, channel="backend-cell", topic="Feature X kickoff",
relationship_type="discussion")
link_session(session_id, task_id, is_primary=False)
```
`relationship_type` is `discussion | planning | review | retrospective`.
`link_session` is idempotent; you must own the task you're linking.
+47 -101
View File
@@ -1,115 +1,61 @@
# Project Tools
# Project & Workspace Tools
## Overview
Project tools manage git repositories and agent workspaces.
There is **no** `roboco_project_*` or `roboco_workspace_*` agent tool.
Agents do **not** create projects, manage git tokens, or ensure
workspaces. Those are handled for you:
## List Projects
- **Workspaces are auto-cloned by the orchestrator** (`WorkspaceService`).
Your per-agent clone of the project repo is created the first time you
claim work on it — you never call a workspace tool. Branches are
auto-created on `i_will_work_on()` / `claim_review()`; you don't run
`git checkout` either.
- **Project registration and git-token management are operator actions**
done through the control panel / HTTP API, not from inside an agent
container. Tokens are encrypted at rest; the agent container never sees
the PAT (it is injected into git operations server-side and scrubbed
from URLs).
## What a task already tells you
A task carries its project linkage; you don't look it up with a tool. The
task object you receive from `give_me_work()` / `triage()` includes the
`project_id` (and the branch the flow verbs check out). Acceptance
criteria and the project context come back inline on the Envelope.
## Inspecting the repo
Read-only git inspection is available through the `roboco-git-readonly`
MCP server (developers and QA):
```python
roboco_project_list() # All accessible projects
roboco_project_list(cell="backend") # Filter by cell
roboco_git_status(project_slug="roboco")
roboco_git_log(project_slug="roboco")
roboco_git_diff(project_slug="roboco")
roboco_git_branch_list(project_slug="roboco")
```
Returns projects you have access to (cell-scoped for non-PMs).
There is **no** `roboco_git_commit / _push / _checkout / _create_pr /
_merge_pr` tool. Commits go through the `commit` content tool (auto-
prefixed with `[task-id]`, auto-pushed by the choreographer); PRs open at
`open_pr` time; merges are a PM `complete` operation.
## Get Project Details
## Finding project knowledge
To learn how a project's codebase is laid out or how a subsystem works,
query the knowledge base rather than a project tool:
```python
roboco_project_get(slug="roboco")
roboco_kb_search(query="rate limiting redis", project="roboco",
index_types=["code", "documentation"])
roboco_ask_mentor(question="How is auth wired up in this project?")
```
Returns: `name`, `git_url`, `assigned_cell`, `default_branch`, `has_git_token`, `test_command`, etc.
## PM note: creating work
**Note:** `has_git_token` indicates if authentication is configured (required for HTTPS repos).
## Create Project (PM+ Only)
```python
# Example: register a separate frontend-only repo as a project.
# (The built-in RoboCo control panel lives in this same repo under
# panel/ and is NOT registered as a separate project.)
roboco_project_create(
name="Customer Portal",
slug="customer-portal",
git_url="https://github.com/org/customer-portal.git",
assigned_cell="frontend",
git_token="ghp_xxxx...", # GitHub PAT with repo scope
default_branch="main",
test_command="pnpm test",
lint_command="pnpm lint"
)
```
**Who can create:** Main PM, Board, CEO
**IMPORTANT:** `git_token` is **required** for HTTPS repositories. Without it, workspace creation and git operations will fail.
## Update Project
```python
roboco_project_update(
slug="roboco-panel",
git_token="ghp_newtoken...", # Update/rotate token
test_command="pnpm test:ci",
lint_command="pnpm lint:fix"
)
```
**Who can update:**
- CEO, Main PM: Any project
- Cell PM: Own cell's projects only
**Token rotation:** Pass `git_token` to update credentials. Pass empty string to clear.
## Workspace Tools
### Ensure Workspace
```python
roboco_workspace_ensure(project_slug="roboco")
```
Creates your workspace if it doesn't exist. Auto-clones the repository.
### Check Workspace Status
```python
roboco_workspace_status(project_slug="roboco")
```
Returns: `exists`, `branch`, `has_uncommitted`, `staged_files`, `unstaged_files`
### List Workspaces (PM Only)
```python
roboco_workspace_list(project_slug="roboco")
```
Lists all agent workspaces for a project. Cell PM sees own cell only.
## Permission Matrix
| Tool | Dev/QA/Doc | Cell PM | Main PM | CEO |
|------|------------|---------|---------|-----|
| `project_list` | Own cell | Own cell | All | All |
| `project_get` | Yes | Yes | Yes | Yes |
| `project_create` | No | No | Yes | Yes |
| `project_update` | No | Own cell | All | All |
| `workspace_ensure` | Yes | Yes | Yes | Yes |
| `workspace_status` | Yes | Yes | Yes | Yes |
| `workspace_list` | No | Own cell | All | All |
## Task Creation with Project
When creating tasks:
```python
roboco_task_create(
title="Add rate limiting",
team="backend",
project_slug="roboco", # Required - all tasks follow git workflow
)
```
Use `project_slug="roboco"` for internal RoboCo codebase work.
PMs create work with the `delegate` flow verb (a subtask under the
current parent task), not a project/task-create tool. `delegate` takes an
optional `project_id`; the parent task's project is inherited when you
omit it. There is no agent-facing standalone project- or task-create
tool.
+107 -86
View File
@@ -1,111 +1,132 @@
# Task Management Tools
## Core Operations
There is **no** `roboco_task_*` tool surface. Tasks move through the
lifecycle via **flow verbs** on the `roboco-flow` MCP server. Each verb is
role-scoped — you only see the ones your role is allowed to call (the
spawn manifest registers them per role). Every verb returns an
**Envelope** whose `next` field tells you what to call next; trust it
rather than guessing state.
| Tool | Purpose |
|------|---------|
| `roboco_task_get` | Get task details |
| `roboco_task_scan` | Find available tasks |
| `roboco_task_claim` | Take ownership |
| `roboco_task_unclaim` | Release claimed task |
| `roboco_task_start` | Begin work |
The verbs below are grouped by who calls them.
## Task Retrieval
## Developer flow
```python
# Get specific task
task = roboco_task_get(task_id)
# Scan for available tasks
tasks = roboco_task_scan(
team="backend", # Optional filter
status="pending" # Optional filter
)
give_me_work() # returns your most-actionable pending task
i_will_work_on(task_id, plan="...")
# claims + sets plan + starts; auto-creates and
# checks out feature/{team}/{task-hierarchy}
commit(message, files=None) # content tool — repeat per change (auto-pushed)
open_pr(task_id) # pushes branch + opens the PR
i_am_done(task_id, notes="") # verifying -> awaiting_qa (PR must already be open)
i_am_blocked(task_id, reason) # external dependency; cell PM unblocks
unclaim(task_id) # release a claimed task back to the queue
resume(task_id) # recover a paused task after compact/restart
i_am_idle() # no work in your queue right now
```
## Task Lifecycle
There is no separate claim / start / pause verb — `i_will_work_on`
composes claim + set-plan + start atomically, and `i_am_done` composes
verify + submit-qa. Branches are auto-created on `i_will_work_on`; do not
checkout by hand.
## QA flow
```python
# Claim task
roboco_task_claim(task_id)
# Release if you shouldn't work on it
roboco_task_unclaim(task_id)
roboco_task_unclaim(task_id, hand_off_to="be-dev-2")
# Start work (also resumes paused tasks)
roboco_task_start(task_id)
# Pause work
roboco_task_pause(task_id, reason="Waiting for clarification")
# Resume paused work (use start)
roboco_task_start(task_id) # Works on paused tasks
# Block (waiting on another task)
roboco_task_block(task_id, blocker_task_id, reason)
# Unblock (PM only)
roboco_task_unblock(task_id)
give_me_work() # returns an awaiting_qa task
claim_review(task_id) # claim for review (auto-checks-out dev branch)
pass(task_id, notes) # awaiting_qa -> awaiting_documentation
fail(task_id, issues=[...]) # awaiting_qa -> needs_revision (dev gets it back)
unclaim(task_id) / resume(task_id) / i_am_idle()
```
## Submission
`notes` (on pass) and `issues` (on fail) must be substantive — the
enforcement layer rejects empty or near-empty content. QA cannot review
its own dev work (self-review guard rejects on `claim_review`).
## Documenter flow
```python
# Submit for verification
roboco_task_submit_verification(task_id)
# Submit for QA
roboco_task_submit_qa(task_id, notes)
# QA actions
roboco_task_qa_pass(task_id, {notes: "..."})
roboco_task_qa_fail(task_id, {notes: "...", issues: [...]})
# Documentation complete
roboco_task_docs_complete(task_id)
# PM complete
roboco_task_complete(task_id)
give_me_work() # returns an awaiting_documentation task
claim_doc_task(task_id) # claim the doc phase
commit(message, files) # commit the doc files you write
i_documented(task_id, notes, files)
# awaiting_documentation -> awaiting_pm_review
```
## PM Operations
Documentation tasks are **not** delegated — the lifecycle auto-creates
the doc phase after a code task passes QA.
## Cell PM flow
```python
# Create SUBTASK (most common)
roboco_task_create({
title: "Implement auth endpoint",
parent_task_id: my_task_id, # REQUIRED for subtasks
team: "backend",
assigned_to: "be-dev-1" # Use SLUG
})
# Create standalone task (rare)
roboco_task_create({
title: "...",
team: "backend",
status: "backlog"
})
# Activate (backlog -> pending)
roboco_task_activate(task_id)
# Cancel
roboco_task_cancel(task_id, reason)
# Plan
roboco_task_plan(task_id, approach, steps)
# Escalate to CEO (parent tasks only)
roboco_task_escalate_to_ceo(task_id, notes)
triage() # list actionable tasks in your cell
i_will_plan(task_id, plan, approach)
# claim + plan + start a parent task
delegate(parent_task_id, title, description, assigned_to, team,
task_type, nature, estimated_complexity, acceptance_criteria)
# create a subtask under the current task
unblock(task_id) # blocked -> in_progress (PM only)
submit_up(task_id, notes) # open cell->root PR; -> awaiting_pm_review
complete(task_id, notes) # awaiting_pm_review -> completed (merges leaf PR)
escalate_up(task_id, reason) # escalate to your escalation target
```
**CRITICAL**: When creating subtasks, ALWAYS include `parent_task_id`. Without it, you create orphan sibling tasks instead of linked subtasks.
**Delegation rules** (enforced): `main_pm -> cell_pm`; `cell_pm -> its
team's devs`. Cell PMs receive planning-typed parent tasks; devs get
code/research (UX devs also design). Always create subtasks via
`delegate` with `parent_task_id` set — there is no standalone task-create
verb for agents.
**Note**: `roboco_task_escalate_to_ceo` only works on parent tasks (tasks without a `parent_task_id`). Subtasks must have their parent task escalated instead.
## Main PM flow
## Progress Updates
The Main PM has the Cell PM verbs **plus**:
```python
roboco_task_progress(task_id, "Implementing API", 50)
triage_all() # list actionable tasks across all teams
escalate_to_ceo(task_id, reason)
# awaiting_pm_review -> awaiting_ceo_approval
give_me_work() # Main PM may also pull work directly
```
`complete` for the Main PM merges the **root** PR. Only the CEO merges to
`master`; agents stop at `escalate_to_ceo`.
## Board flow (Product Owner / Head of Marketing)
```python
triage() # list actionable tasks in scope
escalate_to_ceo(task_id, reason)
i_am_idle()
```
The Board **cannot** claim, create, complete, or cancel tasks. Strategic
decisions are escalated to the CEO.
## Auditor flow
```python
triage() # read-only list of actionable tasks
i_am_idle()
```
The Auditor is a silent observer: read-only `triage`, no `say`/`dm`/
`notify`, no claim/complete/cancel.
## Cancel
Cancelling a task (any non-terminal status -> `cancelled`) is restricted
to **PM roles and the CEO**. There is no agent verb to cancel — it is a
PM/CEO operation through the lifecycle.
## Progress
Record progress against your plan with the `progress` content tool (on
`roboco-do`), not a task verb:
```python
progress(task_id, message="API skeleton landed", plan_step="2")
```
Your plan's steps are the progress checklist; the percentage is derived
from completed steps — you do not set it.