feat: enhance agent and board APIs with role-based tags and improved documentation

This commit is contained in:
Abhimanyu Saharan
2026-02-13 02:08:30 +05:30
parent 5695c0003d
commit a3148baca9
13 changed files with 550 additions and 280 deletions

View File

@@ -21,3 +21,4 @@ ignores:
- "**/.pytest_cache/**" - "**/.pytest_cache/**"
- "**/.mypy_cache/**" - "**/.mypy_cache/**"
- "**/coverage/**" - "**/coverage/**"
- "**/~/**"

View File

@@ -2,7 +2,9 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from typing import cast
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -77,6 +79,11 @@ TASK_STATUS_QUERY = Query(default=None, alias="status")
IS_CHAT_QUERY = Query(default=None) IS_CHAT_QUERY = Query(default=None)
APPROVAL_STATUS_QUERY = Query(default=None, alias="status") APPROVAL_STATUS_QUERY = Query(default=None, alias="status")
AGENT_LEAD_TAGS = cast("list[str | Enum]", ["agent-lead"])
AGENT_MAIN_TAGS = cast("list[str | Enum]", ["agent-main"])
AGENT_BOARD_TAGS = cast("list[str | Enum]", ["agent-lead", "agent-worker"])
AGENT_ALL_ROLE_TAGS = cast("list[str | Enum]", ["agent-lead", "agent-worker", "agent-main"])
def _coerce_agent_items(items: Sequence[Any]) -> list[Agent]: def _coerce_agent_items(items: Sequence[Any]) -> list[Agent]:
agents: list[Agent] = [] agents: list[Agent] = []
@@ -142,12 +149,20 @@ def _guard_task_access(agent_ctx: AgentAuthContext, task: Task) -> None:
OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed) OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed)
@router.get("/boards", response_model=DefaultLimitOffsetPage[BoardRead]) @router.get(
"/boards",
response_model=DefaultLimitOffsetPage[BoardRead],
tags=AGENT_ALL_ROLE_TAGS,
)
async def list_boards( async def list_boards(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> LimitOffsetPage[BoardRead]: ) -> LimitOffsetPage[BoardRead]:
"""List boards visible to the authenticated agent.""" """List boards visible to the authenticated agent.
Board-scoped agents typically see only their assigned board.
Main agents may see multiple boards when permitted by auth scope.
"""
statement = select(Board) statement = select(Board)
if agent_ctx.agent.board_id: if agent_ctx.agent.board_id:
statement = statement.where(col(Board.id) == agent_ctx.agent.board_id) statement = statement.where(col(Board.id) == agent_ctx.agent.board_id)
@@ -155,23 +170,34 @@ async def list_boards(
return await paginate(session, statement) return await paginate(session, statement)
@router.get("/boards/{board_id}", response_model=BoardRead) @router.get("/boards/{board_id}", response_model=BoardRead, tags=AGENT_ALL_ROLE_TAGS)
def get_board( def get_board(
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> Board: ) -> Board:
"""Return a board if the authenticated agent can access it.""" """Return one board if the authenticated agent can access it.
Use this when an agent needs board metadata (objective, status, target date)
before planning or posting updates.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return board return board
@router.get("/agents", response_model=DefaultLimitOffsetPage[AgentRead]) @router.get(
"/agents",
response_model=DefaultLimitOffsetPage[AgentRead],
tags=AGENT_ALL_ROLE_TAGS,
)
async def list_agents( async def list_agents(
board_id: UUID | None = BOARD_ID_QUERY, board_id: UUID | None = BOARD_ID_QUERY,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> LimitOffsetPage[AgentRead]: ) -> LimitOffsetPage[AgentRead]:
"""List agents, optionally filtered to a board.""" """List agents visible to the caller, optionally filtered by board.
Useful for lead delegation and workload balancing.
"""
statement = select(Agent) statement = select(Agent)
if agent_ctx.agent.board_id: if agent_ctx.agent.board_id:
if board_id: if board_id:
@@ -195,14 +221,23 @@ async def list_agents(
return await paginate(session, statement, transformer=_transform) return await paginate(session, statement, transformer=_transform)
@router.get("/boards/{board_id}/tasks", response_model=DefaultLimitOffsetPage[TaskRead]) @router.get(
"/boards/{board_id}/tasks",
response_model=DefaultLimitOffsetPage[TaskRead],
tags=AGENT_BOARD_TAGS,
)
async def list_tasks( async def list_tasks(
filters: AgentTaskListFilters = TASK_LIST_FILTERS_DEP, filters: AgentTaskListFilters = TASK_LIST_FILTERS_DEP,
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> LimitOffsetPage[TaskRead]: ) -> LimitOffsetPage[TaskRead]:
"""List tasks on a board with optional status and assignment filters.""" """List tasks on a board with status/assignment filters.
Common patterns:
- worker: fetch assigned inbox/in-progress tasks
- lead: fetch unassigned inbox tasks for delegation
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return await tasks_api.list_tasks( return await tasks_api.list_tasks(
status_filter=filters.status_filter, status_filter=filters.status_filter,
@@ -214,13 +249,16 @@ async def list_tasks(
) )
@router.get("/boards/{board_id}/tags", response_model=list[TagRef]) @router.get("/boards/{board_id}/tags", response_model=list[TagRef], tags=AGENT_BOARD_TAGS)
async def list_tags( async def list_tags(
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> list[TagRef]: ) -> list[TagRef]:
"""List tags available to the board's organization.""" """List available tags for the board's organization.
Use returned ids in task create/update payloads (`tag_ids`).
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
tags = ( tags = (
await session.exec( await session.exec(
@@ -240,14 +278,18 @@ async def list_tags(
] ]
@router.post("/boards/{board_id}/tasks", response_model=TaskRead) @router.post("/boards/{board_id}/tasks", response_model=TaskRead, tags=AGENT_LEAD_TAGS)
async def create_task( async def create_task(
payload: TaskCreate, payload: TaskCreate,
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> TaskRead: ) -> TaskRead:
"""Create a task on the board as the lead agent.""" """Create a task as the board lead.
Lead-only endpoint. Supports dependency-aware creation via
`depends_on_task_ids` and optional `tag_ids`.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
_require_board_lead(agent_ctx) _require_board_lead(agent_ctx)
data = payload.model_dump(exclude={"depends_on_task_ids", "tag_ids"}) data = payload.model_dump(exclude={"depends_on_task_ids", "tag_ids"})
@@ -343,14 +385,21 @@ async def create_task(
) )
@router.patch("/boards/{board_id}/tasks/{task_id}", response_model=TaskRead) @router.patch(
"/boards/{board_id}/tasks/{task_id}",
response_model=TaskRead,
tags=AGENT_BOARD_TAGS,
)
async def update_task( async def update_task(
payload: TaskUpdate, payload: TaskUpdate,
task: Task = TASK_DEP, task: Task = TASK_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> TaskRead: ) -> TaskRead:
"""Update a task after board-level access checks.""" """Update a task after board-level authorization checks.
Supports status, assignment, dependencies, and optional inline comment.
"""
_guard_task_access(agent_ctx, task) _guard_task_access(agent_ctx, task)
return await tasks_api.update_task( return await tasks_api.update_task(
payload=payload, payload=payload,
@@ -363,13 +412,17 @@ async def update_task(
@router.get( @router.get(
"/boards/{board_id}/tasks/{task_id}/comments", "/boards/{board_id}/tasks/{task_id}/comments",
response_model=DefaultLimitOffsetPage[TaskCommentRead], response_model=DefaultLimitOffsetPage[TaskCommentRead],
tags=AGENT_BOARD_TAGS,
) )
async def list_task_comments( async def list_task_comments(
task: Task = TASK_DEP, task: Task = TASK_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> LimitOffsetPage[TaskCommentRead]: ) -> LimitOffsetPage[TaskCommentRead]:
"""List comments for a task visible to the authenticated agent.""" """List task comments visible to the authenticated agent.
Read this before posting updates to avoid duplicate or low-value comments.
"""
_guard_task_access(agent_ctx, task) _guard_task_access(agent_ctx, task)
return await tasks_api.list_task_comments( return await tasks_api.list_task_comments(
task=task, task=task,
@@ -380,6 +433,7 @@ async def list_task_comments(
@router.post( @router.post(
"/boards/{board_id}/tasks/{task_id}/comments", "/boards/{board_id}/tasks/{task_id}/comments",
response_model=TaskCommentRead, response_model=TaskCommentRead,
tags=AGENT_BOARD_TAGS,
) )
async def create_task_comment( async def create_task_comment(
payload: TaskCommentCreate, payload: TaskCommentCreate,
@@ -387,7 +441,10 @@ async def create_task_comment(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> ActivityEvent: ) -> ActivityEvent:
"""Create a task comment on behalf of the authenticated agent.""" """Create a task comment as the authenticated agent.
This is the primary collaboration/log surface for task progress.
"""
_guard_task_access(agent_ctx, task) _guard_task_access(agent_ctx, task)
return await tasks_api.create_task_comment( return await tasks_api.create_task_comment(
payload=payload, payload=payload,
@@ -400,6 +457,7 @@ async def create_task_comment(
@router.get( @router.get(
"/boards/{board_id}/memory", "/boards/{board_id}/memory",
response_model=DefaultLimitOffsetPage[BoardMemoryRead], response_model=DefaultLimitOffsetPage[BoardMemoryRead],
tags=AGENT_BOARD_TAGS,
) )
async def list_board_memory( async def list_board_memory(
is_chat: bool | None = IS_CHAT_QUERY, is_chat: bool | None = IS_CHAT_QUERY,
@@ -407,7 +465,10 @@ async def list_board_memory(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> LimitOffsetPage[BoardMemoryRead]: ) -> LimitOffsetPage[BoardMemoryRead]:
"""List board memory entries with optional chat filtering.""" """List board memory with optional chat filtering.
Use `is_chat=false` for durable context and `is_chat=true` for board chat.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return await board_memory_api.list_board_memory( return await board_memory_api.list_board_memory(
is_chat=is_chat, is_chat=is_chat,
@@ -417,14 +478,17 @@ async def list_board_memory(
) )
@router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead) @router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead, tags=AGENT_BOARD_TAGS)
async def create_board_memory( async def create_board_memory(
payload: BoardMemoryCreate, payload: BoardMemoryCreate,
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> BoardMemory: ) -> BoardMemory:
"""Create a board memory entry.""" """Create a board memory entry.
Use tags to indicate purpose (e.g. `chat`, `decision`, `plan`, `handoff`).
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return await board_memory_api.create_board_memory( return await board_memory_api.create_board_memory(
payload=payload, payload=payload,
@@ -437,6 +501,7 @@ async def create_board_memory(
@router.get( @router.get(
"/boards/{board_id}/approvals", "/boards/{board_id}/approvals",
response_model=DefaultLimitOffsetPage[ApprovalRead], response_model=DefaultLimitOffsetPage[ApprovalRead],
tags=AGENT_BOARD_TAGS,
) )
async def list_approvals( async def list_approvals(
status_filter: ApprovalStatus | None = APPROVAL_STATUS_QUERY, status_filter: ApprovalStatus | None = APPROVAL_STATUS_QUERY,
@@ -444,7 +509,10 @@ async def list_approvals(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> LimitOffsetPage[ApprovalRead]: ) -> LimitOffsetPage[ApprovalRead]:
"""List approvals for a board.""" """List approvals for a board.
Use status filtering to process pending approvals efficiently.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return await approvals_api.list_approvals( return await approvals_api.list_approvals(
status_filter=status_filter, status_filter=status_filter,
@@ -454,14 +522,17 @@ async def list_approvals(
) )
@router.post("/boards/{board_id}/approvals", response_model=ApprovalRead) @router.post("/boards/{board_id}/approvals", response_model=ApprovalRead, tags=AGENT_BOARD_TAGS)
async def create_approval( async def create_approval(
payload: ApprovalCreate, payload: ApprovalCreate,
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> ApprovalRead: ) -> ApprovalRead:
"""Create a board approval request.""" """Create an approval request for risky or low-confidence actions.
Include `task_id` or `task_ids` to scope the decision precisely.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return await approvals_api.create_approval( return await approvals_api.create_approval(
payload=payload, payload=payload,
@@ -471,14 +542,21 @@ async def create_approval(
) )
@router.post("/boards/{board_id}/onboarding", response_model=BoardOnboardingRead) @router.post(
"/boards/{board_id}/onboarding",
response_model=BoardOnboardingRead,
tags=AGENT_BOARD_TAGS,
)
async def update_onboarding( async def update_onboarding(
payload: BoardOnboardingAgentUpdate, payload: BoardOnboardingAgentUpdate,
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
"""Apply onboarding updates for a board.""" """Apply board onboarding updates from an agent workflow.
Used during structured objective/success-metric intake loops.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return await onboarding_api.agent_onboarding_update( return await onboarding_api.agent_onboarding_update(
payload=payload, payload=payload,
@@ -488,13 +566,16 @@ async def update_onboarding(
) )
@router.post("/agents", response_model=AgentRead) @router.post("/agents", response_model=AgentRead, tags=AGENT_LEAD_TAGS)
async def create_agent( async def create_agent(
payload: AgentCreate, payload: AgentCreate,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> AgentRead: ) -> AgentRead:
"""Create an agent on the caller's board.""" """Create a new board agent as lead.
The new agent is always forced onto the caller's board (`board_id` override).
"""
lead = _require_board_lead(agent_ctx) lead = _require_board_lead(agent_ctx)
payload = AgentCreate( payload = AgentCreate(
**{**payload.model_dump(), "board_id": lead.board_id}, **{**payload.model_dump(), "board_id": lead.board_id},
@@ -506,7 +587,11 @@ async def create_agent(
) )
@router.post("/boards/{board_id}/agents/{agent_id}/nudge", response_model=OkResponse) @router.post(
"/boards/{board_id}/agents/{agent_id}/nudge",
response_model=OkResponse,
tags=AGENT_LEAD_TAGS,
)
async def nudge_agent( async def nudge_agent(
payload: AgentNudge, payload: AgentNudge,
agent_id: str, agent_id: str,
@@ -514,7 +599,10 @@ async def nudge_agent(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> OkResponse: ) -> OkResponse:
"""Send a direct nudge message to a board agent.""" """Send a direct nudge to one board agent.
Lead-only endpoint for stale or blocked in-progress work.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
_require_board_lead(agent_ctx) _require_board_lead(agent_ctx)
coordination = GatewayCoordinationService(session) coordination = GatewayCoordinationService(session)
@@ -528,13 +616,16 @@ async def nudge_agent(
return OkResponse() return OkResponse()
@router.post("/heartbeat", response_model=AgentRead) @router.post("/heartbeat", response_model=AgentRead, tags=AGENT_ALL_ROLE_TAGS)
async def agent_heartbeat( async def agent_heartbeat(
payload: AgentHeartbeatCreate, payload: AgentHeartbeatCreate,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> AgentRead: ) -> AgentRead:
"""Record heartbeat status for the authenticated agent.""" """Record heartbeat status for the authenticated agent.
Heartbeats are identity-bound to the token's agent id.
"""
# Heartbeats must apply to the authenticated agent; agent names are not unique. # Heartbeats must apply to the authenticated agent; agent names are not unique.
return await agents_api.heartbeat_agent( return await agents_api.heartbeat_agent(
agent_id=str(agent_ctx.agent.id), agent_id=str(agent_ctx.agent.id),
@@ -544,14 +635,21 @@ async def agent_heartbeat(
) )
@router.get("/boards/{board_id}/agents/{agent_id}/soul", response_model=str) @router.get(
"/boards/{board_id}/agents/{agent_id}/soul",
response_model=str,
tags=AGENT_BOARD_TAGS,
)
async def get_agent_soul( async def get_agent_soul(
agent_id: str, agent_id: str,
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> str: ) -> str:
"""Fetch the target agent's SOUL.md content from the gateway.""" """Fetch an agent's SOUL.md content.
Allowed for board lead, or for an agent reading its own SOUL.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
OpenClawAuthorizationPolicy.require_board_lead_or_same_actor( OpenClawAuthorizationPolicy.require_board_lead_or_same_actor(
actor_agent=agent_ctx.agent, actor_agent=agent_ctx.agent,
@@ -565,7 +663,11 @@ async def get_agent_soul(
) )
@router.put("/boards/{board_id}/agents/{agent_id}/soul", response_model=OkResponse) @router.put(
"/boards/{board_id}/agents/{agent_id}/soul",
response_model=OkResponse,
tags=AGENT_LEAD_TAGS,
)
async def update_agent_soul( async def update_agent_soul(
agent_id: str, agent_id: str,
payload: SoulUpdateRequest, payload: SoulUpdateRequest,
@@ -573,7 +675,10 @@ async def update_agent_soul(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> OkResponse: ) -> OkResponse:
"""Update an agent's SOUL.md content in DB and gateway.""" """Update an agent's SOUL.md template in DB and gateway.
Lead-only endpoint. Persists as `soul_template` for future reprovisioning.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
_require_board_lead(agent_ctx) _require_board_lead(agent_ctx)
coordination = GatewayCoordinationService(session) coordination = GatewayCoordinationService(session)
@@ -589,14 +694,21 @@ async def update_agent_soul(
return OkResponse() return OkResponse()
@router.delete("/boards/{board_id}/agents/{agent_id}", response_model=OkResponse) @router.delete(
"/boards/{board_id}/agents/{agent_id}",
response_model=OkResponse,
tags=AGENT_LEAD_TAGS,
)
async def delete_board_agent( async def delete_board_agent(
agent_id: str, agent_id: str,
board: Board = BOARD_DEP, board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> OkResponse: ) -> OkResponse:
"""Delete a board agent as the board lead.""" """Delete a board agent as board lead.
Cleans up runtime/session state through lifecycle services.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
_require_board_lead(agent_ctx) _require_board_lead(agent_ctx)
service = AgentLifecycleService(session) service = AgentLifecycleService(session)
@@ -609,6 +721,7 @@ async def delete_board_agent(
@router.post( @router.post(
"/boards/{board_id}/gateway/main/ask-user", "/boards/{board_id}/gateway/main/ask-user",
response_model=GatewayMainAskUserResponse, response_model=GatewayMainAskUserResponse,
tags=AGENT_LEAD_TAGS,
) )
async def ask_user_via_gateway_main( async def ask_user_via_gateway_main(
payload: GatewayMainAskUserRequest, payload: GatewayMainAskUserRequest,
@@ -616,7 +729,10 @@ async def ask_user_via_gateway_main(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> GatewayMainAskUserResponse: ) -> GatewayMainAskUserResponse:
"""Route a lead's ask-user request through the dedicated gateway agent.""" """Ask the human via gateway-main external channels.
Lead-only endpoint for situations where board chat is not responsive.
"""
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
_require_board_lead(agent_ctx) _require_board_lead(agent_ctx)
coordination = GatewayCoordinationService(session) coordination = GatewayCoordinationService(session)
@@ -630,6 +746,7 @@ async def ask_user_via_gateway_main(
@router.post( @router.post(
"/gateway/boards/{board_id}/lead/message", "/gateway/boards/{board_id}/lead/message",
response_model=GatewayLeadMessageResponse, response_model=GatewayLeadMessageResponse,
tags=AGENT_MAIN_TAGS,
) )
async def message_gateway_board_lead( async def message_gateway_board_lead(
board_id: UUID, board_id: UUID,
@@ -637,7 +754,7 @@ async def message_gateway_board_lead(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> GatewayLeadMessageResponse: ) -> GatewayLeadMessageResponse:
"""Send a gateway-main message to a single board lead agent.""" """Send a gateway-main control message to one board lead."""
coordination = GatewayCoordinationService(session) coordination = GatewayCoordinationService(session)
return await coordination.message_gateway_board_lead( return await coordination.message_gateway_board_lead(
actor_agent=agent_ctx.agent, actor_agent=agent_ctx.agent,
@@ -649,13 +766,14 @@ async def message_gateway_board_lead(
@router.post( @router.post(
"/gateway/leads/broadcast", "/gateway/leads/broadcast",
response_model=GatewayLeadBroadcastResponse, response_model=GatewayLeadBroadcastResponse,
tags=AGENT_MAIN_TAGS,
) )
async def broadcast_gateway_lead_message( async def broadcast_gateway_lead_message(
payload: GatewayLeadBroadcastRequest, payload: GatewayLeadBroadcastRequest,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> GatewayLeadBroadcastResponse: ) -> GatewayLeadBroadcastResponse:
"""Broadcast a gateway-main message to multiple board leads.""" """Broadcast a gateway-main control message to multiple board leads."""
coordination = GatewayCoordinationService(session) coordination = GatewayCoordinationService(session)
return await coordination.broadcast_gateway_lead_message( return await coordination.broadcast_gateway_lead_message(
actor_agent=agent_ctx.agent, actor_agent=agent_ctx.agent,

View File

@@ -4,9 +4,11 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import cast
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
@@ -68,6 +70,7 @@ ACTOR_DEP = Depends(require_admin_or_agent)
IS_CHAT_QUERY = Query(default=None) IS_CHAT_QUERY = Query(default=None)
SINCE_QUERY = Query(default=None) SINCE_QUERY = Query(default=None)
_RUNTIME_TYPE_REFERENCES = (UUID,) _RUNTIME_TYPE_REFERENCES = (UUID,)
AGENT_BOARD_ROLE_TAGS = cast("list[str | Enum]", ["agent-lead", "agent-worker"])
def _parse_since(value: str | None) -> datetime | None: def _parse_since(value: str | None) -> datetime | None:
@@ -402,14 +405,21 @@ async def create_board_group_memory(
return memory return memory
@board_router.get("", response_model=DefaultLimitOffsetPage[BoardGroupMemoryRead]) @board_router.get(
"",
response_model=DefaultLimitOffsetPage[BoardGroupMemoryRead],
tags=AGENT_BOARD_ROLE_TAGS,
)
async def list_board_group_memory_for_board( async def list_board_group_memory_for_board(
*, *,
is_chat: bool | None = IS_CHAT_QUERY, is_chat: bool | None = IS_CHAT_QUERY,
board: Board = BOARD_READ_DEP, board: Board = BOARD_READ_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
) -> LimitOffsetPage[BoardGroupMemoryRead]: ) -> LimitOffsetPage[BoardGroupMemoryRead]:
"""List memory entries for the board's linked group.""" """List shared memory for the board's linked group.
Use this for cross-board context and coordination signals.
"""
group_id = board.board_group_id group_id = board.board_group_id
if group_id is None: if group_id is None:
return await paginate(session, BoardGroupMemory.objects.by_ids([]).statement) return await paginate(session, BoardGroupMemory.objects.by_ids([]).statement)
@@ -426,7 +436,7 @@ async def list_board_group_memory_for_board(
return await paginate(session, queryset.statement) return await paginate(session, queryset.statement)
@board_router.get("/stream") @board_router.get("/stream", tags=AGENT_BOARD_ROLE_TAGS)
async def stream_board_group_memory_for_board( async def stream_board_group_memory_for_board(
request: Request, request: Request,
*, *,
@@ -434,7 +444,7 @@ async def stream_board_group_memory_for_board(
since: str | None = SINCE_QUERY, since: str | None = SINCE_QUERY,
is_chat: bool | None = IS_CHAT_QUERY, is_chat: bool | None = IS_CHAT_QUERY,
) -> EventSourceResponse: ) -> EventSourceResponse:
"""Stream memory entries for the board's linked group.""" """Stream linked-group memory via SSE for near-real-time coordination."""
group_id = board.board_group_id group_id = board.board_group_id
since_dt = _parse_since(since) or utcnow() since_dt = _parse_since(since) or utcnow()
last_seen = since_dt last_seen = since_dt
@@ -463,14 +473,18 @@ async def stream_board_group_memory_for_board(
return EventSourceResponse(event_generator(), ping=15) return EventSourceResponse(event_generator(), ping=15)
@board_router.post("", response_model=BoardGroupMemoryRead) @board_router.post("", response_model=BoardGroupMemoryRead, tags=AGENT_BOARD_ROLE_TAGS)
async def create_board_group_memory_for_board( async def create_board_group_memory_for_board(
payload: BoardGroupMemoryCreate, payload: BoardGroupMemoryCreate,
board: Board = BOARD_WRITE_DEP, board: Board = BOARD_WRITE_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
actor: ActorContext = ACTOR_DEP, actor: ActorContext = ACTOR_DEP,
) -> BoardGroupMemory: ) -> BoardGroupMemory:
"""Create a group memory entry from a board context and notify recipients.""" """Create shared group memory from a board context.
When tags/mentions indicate chat or broadcast intent, eligible agents in the
linked group are notified.
"""
group_id = board.board_group_id group_id = board.board_group_id
if group_id is None: if group_id is None:
raise HTTPException( raise HTTPException(

View File

@@ -2,7 +2,9 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from typing import cast
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -56,6 +58,7 @@ BOARD_GROUP_ID_QUERY = Query(default=None)
INCLUDE_SELF_QUERY = Query(default=False) INCLUDE_SELF_QUERY = Query(default=False)
INCLUDE_DONE_QUERY = Query(default=False) INCLUDE_DONE_QUERY = Query(default=False)
PER_BOARD_TASK_LIMIT_QUERY = Query(default=5, ge=0, le=100) PER_BOARD_TASK_LIMIT_QUERY = Query(default=5, ge=0, le=100)
AGENT_BOARD_ROLE_TAGS = cast("list[str | Enum]", ["agent-lead", "agent-worker"])
async def _require_gateway( async def _require_gateway(
@@ -393,7 +396,11 @@ async def get_board_snapshot(
return await build_board_snapshot(session, board) return await build_board_snapshot(session, board)
@router.get("/{board_id}/group-snapshot", response_model=BoardGroupSnapshot) @router.get(
"/{board_id}/group-snapshot",
response_model=BoardGroupSnapshot,
tags=AGENT_BOARD_ROLE_TAGS,
)
async def get_board_group_snapshot( async def get_board_group_snapshot(
*, *,
include_self: bool = INCLUDE_SELF_QUERY, include_self: bool = INCLUDE_SELF_QUERY,
@@ -402,7 +409,10 @@ async def get_board_group_snapshot(
board: Board = BOARD_ACTOR_READ_DEP, board: Board = BOARD_ACTOR_READ_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
) -> BoardGroupSnapshot: ) -> BoardGroupSnapshot:
"""Get a grouped snapshot across related boards.""" """Get a grouped snapshot across related boards.
Returns high-signal cross-board status for dependency and overlap checks.
"""
return await build_board_group_snapshot( return await build_board_group_snapshot(
session, session,
board=board, board=board,

View File

@@ -38,6 +38,36 @@ if TYPE_CHECKING:
configure_logging() configure_logging()
logger = get_logger(__name__) logger = get_logger(__name__)
OPENAPI_TAGS = [
{
"name": "agent",
"description": (
"Agent-scoped API surface. All endpoints require `X-Agent-Token` and are "
"constrained by agent board access policies."
),
},
{
"name": "agent-lead",
"description": (
"Lead workflows: delegation, review orchestration, approvals, and "
"coordination actions."
),
},
{
"name": "agent-worker",
"description": (
"Worker workflows: task execution, task comments, and board/group context "
"reads/writes used during heartbeat loops."
),
},
{
"name": "agent-main",
"description": (
"Gateway-main control workflows that message board leads or broadcast "
"coordination requests."
),
},
]
@asynccontextmanager @asynccontextmanager
@@ -56,7 +86,12 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
logger.info("app.lifecycle.stopped") logger.info("app.lifecycle.stopped")
app = FastAPI(title="Mission Control API", version="0.1.0", lifespan=lifespan) app = FastAPI(
title="Mission Control API",
version="0.1.0",
lifespan=lifespan,
openapi_tags=OPENAPI_TAGS,
)
origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()] origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
if origins: if origins:

View File

@@ -55,6 +55,7 @@ DEFAULT_GATEWAY_FILES = frozenset(
{ {
"AGENTS.md", "AGENTS.md",
"SOUL.md", "SOUL.md",
"LEAD_PLAYBOOK.md",
"TASK_SOUL.md", "TASK_SOUL.md",
"SELF.md", "SELF.md",
"AUTONOMY.md", "AUTONOMY.md",

View File

@@ -12,6 +12,37 @@ Goal: do real work with low noise while sharing useful knowledge across the boar
If any required input is missing, stop and request a provisioning update. If any required input is missing, stop and request a provisioning update.
## API source of truth (OpenAPI)
Use OpenAPI for endpoint/payload details instead of relying on static examples.
```bash
curl -s "$BASE_URL/openapi.json" -o /tmp/openapi.json
```
List operations with role tags:
```bash
jq -r '
.paths | to_entries[] | .key as $path
| .value | to_entries[]
| select(any((.value.tags // [])[]; startswith("agent-")))
| ((.value.summary // "") | gsub("\\s+"; " ")) as $summary
| ((.value.description // "") | split("\n")[0] | gsub("\\s+"; " ")) as $desc
| "\(.key|ascii_upcase)\t\([(.value.tags // [])[] | select(startswith("agent-"))] | join(","))\t\($path)\t\($summary)\t\($desc)"
' /tmp/openapi.json | sort
```
Worker-focused filter (no path regex needed):
```bash
jq -r '
.paths | to_entries[] | .key as $path
| .value | to_entries[]
| select((.value.tags // []) | index("agent-worker"))
| ((.value.summary // "") | gsub("\\s+"; " ")) as $summary
| ((.value.description // "") | split("\n")[0] | gsub("\\s+"; " ")) as $desc
| "\(.key|ascii_upcase)\t\($path)\t\($summary)\t\($desc)"
' /tmp/openapi.json | sort
```
## Schedule ## Schedule
- Schedule is controlled by gateway heartbeat config (default: every 10 minutes). - Schedule is controlled by gateway heartbeat config (default: every 10 minutes).
- Keep cadence conservative unless there is a clear latency need. - Keep cadence conservative unless there is a clear latency need.
@@ -71,36 +102,18 @@ If any required input is missing, stop and request a provisioning update.
## Heartbeat checklist (run in order) ## Heartbeat checklist (run in order)
1) Check in: 1) Check in:
```bash - Use `POST /api/v1/agent/heartbeat`.
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
-H "X-Agent-Token: {{ auth_token }}" \
-H "Content-Type: application/json" \
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
```
2) Pull execution context: 2) Pull execution context:
```bash - Use `agent-worker` endpoints from OpenAPI for:
curl -s "$BASE_URL/api/v1/agent/agents?board_id=$BOARD_ID" \ - board agents list,
-H "X-Agent-Token: {{ auth_token }}" - assigned `in_progress` tasks,
``` - assigned `inbox` tasks.
```bash
curl -s "$BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks?status=in_progress&assigned_agent_id=$AGENT_ID&limit=5" \
-H "X-Agent-Token: {{ auth_token }}"
```
```bash
curl -s "$BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks?status=inbox&assigned_agent_id=$AGENT_ID&limit=10" \
-H "X-Agent-Token: {{ auth_token }}"
```
3) Pull shared knowledge before execution: 3) Pull shared knowledge before execution:
```bash - Use `agent-worker` endpoints from OpenAPI for:
curl -s "$BASE_URL/api/v1/agent/boards/$BOARD_ID/memory?is_chat=false&limit=50" \ - board memory (`is_chat=false`),
-H "X-Agent-Token: {{ auth_token }}" - group memory (if grouped).
```
```bash
curl -s "$BASE_URL/api/v1/boards/$BOARD_ID/group-memory?limit=50" \
-H "X-Agent-Token: {{ auth_token }}"
```
- If the board is not in a group, group-memory may return no group; continue. - If the board is not in a group, group-memory may return no group; continue.
4) Choose work: 4) Choose work:
@@ -162,12 +175,7 @@ If there is no high-value assist available, write one non-chat board memory note
If there are no pending tasks to assist (no meaningful `in_progress`/`review` opportunities): If there are no pending tasks to assist (no meaningful `in_progress`/`review` opportunities):
1) Ask `@lead` for new work on board chat: 1) Ask `@lead` for new work on board chat:
```bash - Post to board chat memory endpoint with `tags:["chat"]` and include `@lead`.
curl -s -X POST "$BASE_URL/api/v1/agent/boards/$BOARD_ID/memory" \
-H "X-Agent-Token: {{ auth_token }}" \
-H "Content-Type: application/json" \
-d '{"content":"@lead I have no actionable tasks/assists right now. Please add/assign next work.","tags":["chat"]}'
```
2) In the same message (or a short follow-up), suggest 1-3 concrete next tasks that would move the board forward. 2) In the same message (or a short follow-up), suggest 1-3 concrete next tasks that would move the board forward.
3) Keep suggestions concise and outcome-oriented (title + why it matters + expected artifact). 3) Keep suggestions concise and outcome-oriented (title + why it matters + expected artifact).

View File

@@ -12,28 +12,57 @@ You are the lead agent for this board. You delegate work; you do not execute tas
If any required input is missing, stop and request a provisioning update. If any required input is missing, stop and request a provisioning update.
## API source of truth (OpenAPI)
Use OpenAPI for endpoint and payload details. This file defines behavior/policy;
OpenAPI defines request/response shapes.
```bash
curl -s "$BASE_URL/openapi.json" -o /tmp/openapi.json
```
List operations with role tags:
```bash
jq -r '
.paths | to_entries[] | .key as $path
| .value | to_entries[]
| select(any((.value.tags // [])[]; startswith("agent-")))
| ((.value.summary // "") | gsub("\\s+"; " ")) as $summary
| ((.value.description // "") | split("\n")[0] | gsub("\\s+"; " ")) as $desc
| "\(.key|ascii_upcase)\t\([(.value.tags // [])[] | select(startswith("agent-"))] | join(","))\t\($path)\t\($summary)\t\($desc)"
' /tmp/openapi.json | sort
```
Lead-focused filter (no path regex needed):
```bash
jq -r '
.paths | to_entries[] | .key as $path
| .value | to_entries[]
| select((.value.tags // []) | index("agent-lead"))
| ((.value.summary // "") | gsub("\\s+"; " ")) as $summary
| ((.value.description // "") | split("\n")[0] | gsub("\\s+"; " ")) as $desc
| "\(.key|ascii_upcase)\t\($path)\t\($summary)\t\($desc)"
' /tmp/openapi.json | sort
```
## Schedule ## Schedule
- Schedule is controlled by gateway heartbeat config (default: every 10 minutes). - Schedule is controlled by gateway heartbeat config (default: every 10 minutes).
- On first boot, send one immediate check-in before the schedule starts. - On first boot, send one immediate check-in before the schedule starts.
## Nonnegotiable rules ## Nonnegotiable rules
- The lead agent must **never** work a task directly. - Never execute tasks directly as lead.
- Do **not** claim tasks. Do **not** post task comments **except** to leave review feedback, respond to a @mention, add clarifying questions on tasks you created, or leave a short coordination note to de-duplicate overlapping tasks (to prevent parallel wasted work). - Do not claim tasks.
- The lead only **delegates**, **requests approvals**, **updates board memory**, **nudges agents**, and **adds review feedback**. - Lead actions are delegation, approvals, board memory updates, nudges, and review feedback.
- All outputs must go to Mission Control via HTTP (never chat/web). - Keep communication low-noise and state-change focused.
- Keep communication low-noise: avoid repetitive status updates and prefer state-change updates. - Never idle: if no actionable tasks exist, create/delegate the next best tasks.
- You are responsible for **proactively driving the board toward its goal** every heartbeat. This means you continuously identify what is missing, what is blocked, and what should happen next to move the objective forward. You do not wait for humans to ask; you create momentum by proposing and delegating the next best work. - Prevent duplicate work: one DRI per deliverable.
- **Never idle.** If there are no pending tasks (no inbox / in_progress / review items), you must create a concrete plan and populate the board with the next best tasks to achieve the goal. - Increase collaboration using Assist tasks and buddy checks for high-priority work.
- You are responsible for **increasing collaboration among other agents**. Look for opportunities to break work into smaller pieces, pair complementary skills, and keep agents aligned on shared outcomes. When you see gaps, create or approve the tasks that connect individual efforts to the bigger picture. - Use board/group memory as the shared knowledge bus.
- Board memory and group memory are the knowledge bus. Synthesize reusable insights there so agents learn from each other without task-comment spam. - Ensure delegated tasks include a clear task lens for `TASK_SOUL.md`.
- Enforce task-adaptive behavior: each delegated task should include a clear "task lens" (mission, audience, artifact, quality bar, constraints) so assignees can update `TASK_SOUL.md` and adapt. - Task comments are limited to review feedback, mentions, tasks you created, and short de-dup notes.
- Prevent duplicate parallel work. Before you create tasks or approvals (and before you delegate a set of tasks), scan existing tasks + board memory for overlap and explicitly merge/split scope so only one agent is the DRI for any given deliverable. - Keep comments concise, actionable, and net-new.
- Prefer "Assist" tasks over reassigning. If a task is in_progress and needs help, create a separate Assist task assigned to an idle agent with a single deliverable: leave a concrete, helpful comment on the original task thread. - For human input, use board chat or approvals (not task-comment `@lead` questions).
- Ensure every high-priority task has a second set of eyes: a buddy agent for review, validation, or risk/edge-case checks (again via Assist tasks). - All outputs go via Mission Control HTTP only.
- When you comment on a task (review feedback, @mentions, tasks you created), keep it concise and actionable with net-new information only. - Do not respond in OpenClaw chat.
- Do **not** include `Questions for @lead` (you are the lead). If you need to ask another agent a question, add a `Questions` section and @mention the assignee (or another agent). If you need human input/decision, ask in board chat or request an approval (not in task comments).
- When you leave review feedback, format it as clean markdown. Use headings/bullets/tables when helpful, but only when it improves clarity.
- If your feedback is longer than 2 sentences, do **not** write a single paragraph. Use a short heading + bullets so each idea is on its own line.
Comment template (keep it small; 1-3 bullets per section): Comment template (keep it small; 1-3 bullets per section):
```md ```md
@@ -57,24 +86,21 @@ Comment template (keep it small; 1-3 bullets per section):
## Board chat messages ## Board chat messages
- If you receive a BOARD CHAT message or BOARD CHAT MENTION message, reply in board chat. - If you receive a BOARD CHAT message or BOARD CHAT MENTION message, reply in board chat.
- Use: POST $BASE_URL/api/v1/agent/boards/$BOARD_ID/memory - Use the `agent-lead` board memory create endpoint (`tags:["chat"]`).
Body: {"content":"...","tags":["chat"]}
- Board chat is your primary channel with the human; respond promptly and clearly. - Board chat is your primary channel with the human; respond promptly and clearly.
- If someone asks for clarity by tagging `@lead`, respond with a crisp decision, delegation, or next action to unblock them. - If someone asks for clarity by tagging `@lead`, respond with a crisp decision, delegation, or next action to unblock them.
- If you issue a directive intended for all non-lead agents, mark it clearly (e.g., "ALL AGENTS") and require one-line acknowledgements from each non-lead agent. - If you issue a directive intended for all non-lead agents, mark it clearly (e.g., "ALL AGENTS") and require one-line acknowledgements from each non-lead agent.
## Request user input via gateway main (OpenClaw channels) ## Request user input via gateway main (OpenClaw channels)
- If you need information from the human but they are not responding in Mission Control board chat, ask the gateway main agent to reach them via OpenClaw's configured channel(s) (Slack/Telegram/SMS/etc). - If you need information from the human but they are not responding in Mission Control board chat, ask the gateway main agent to reach them via OpenClaw's configured channel(s) (Slack/Telegram/SMS/etc).
- POST `$BASE_URL/api/v1/agent/boards/$BOARD_ID/gateway/main/ask-user` - Use the `agent-lead` gateway-main ask-user endpoint.
- Body: `{"content":"<question>","correlation_id":"<optional>","preferred_channel":"<optional>"}`
- The gateway main will post the user's answer back to this board as a NON-chat memory item tagged like `["gateway_main","user_reply"]`. - The gateway main will post the user's answer back to this board as a NON-chat memory item tagged like `["gateway_main","user_reply"]`.
## Gateway main requests ## Gateway main requests
- If you receive a message starting with `GATEWAY MAIN`, treat it as high priority. - If you receive a message starting with `GATEWAY MAIN`, treat it as high priority.
- Do **not** reply in OpenClaw chat. Reply via Mission Control only. - Do **not** reply in OpenClaw chat. Reply via Mission Control only.
- For questions: answer in a NON-chat memory item on this board (so the gateway main can read it): - For questions: answer in a NON-chat memory item on this board (so the gateway main can read it):
- POST `$BASE_URL/api/v1/agent/boards/$BOARD_ID/memory` - Use board memory create with tags like `["gateway_main","lead_reply"]`.
- Body: `{"content":"...","tags":["gateway_main","lead_reply"],"source":"lead_to_gateway_main"}`
- For handoffs: delegate the work on this board (create/triage tasks, assign agents), then post: - For handoffs: delegate the work on this board (create/triage tasks, assign agents), then post:
- A short acknowledgement + plan as a NON-chat memory item using the same tags. - A short acknowledgement + plan as a NON-chat memory item using the same tags.
@@ -110,32 +136,16 @@ run a short intake with the human in **board chat**.
### Checklist ### Checklist
1) Check if intake already exists so you do not spam: 1) Check if intake already exists so you do not spam:
- GET `$BASE_URL/api/v1/agent/boards/$BOARD_ID/memory?limit=200` - Query board memory via `agent-lead` endpoints.
- If you find a **non-chat** memory item tagged `intake`, do not ask again. - If you find a **non-chat** memory item tagged `intake`, do not ask again.
2) Ask **3-7 targeted questions** in a single board chat message: 2) Ask **3-7 targeted questions** in a single board chat message:
- POST `$BASE_URL/api/v1/agent/boards/$BOARD_ID/memory` - Post one board chat message (`tags:["chat"]`) via `agent-lead` memory endpoint.
Body: `{"content":"...","tags":["chat"],"source":"lead_intake"}` - For question bank/examples, see `LEAD_PLAYBOOK.md`.
Question bank (pick only what's needed; keep total <= 7):
1. Objective: What is the single most important outcome? (1-2 sentences)
2. Success metrics: What are 3-5 measurable indicators that were done?
3. Deadline: Is there a target date or milestone dates? (and whats driving them)
4. Constraints: Budget/tools/brand/technical constraints we must respect?
5. Scope: What is explicitly out of scope?
6. Stakeholders: Who approves the final outcome? Anyone else to keep informed?
7. Update preference: How often do you want updates (daily/weekly/asap) and how detailed?
Suggested message template:
- "To confirm the goal, I need a few quick inputs:"
- "1) ..."
- "2) ..."
- "3) ..."
3) When the human answers, **consolidate** the answers: 3) When the human answers, **consolidate** the answers:
- Write a structured summary into board memory: - Write a structured summary into board memory:
- POST `$BASE_URL/api/v1/agent/boards/$BOARD_ID/memory` - Use non-chat memory with tags like `["intake","goal","lead"]`.
Body: `{"content":"<summary>","tags":["intake","goal","lead"],"source":"lead_intake_summary"}`
- Also append the same summary under `## Intake notes (lead)` in `USER.md` (workspace doc). - Also append the same summary under `## Intake notes (lead)` in `USER.md` (workspace doc).
4) Only after intake: 4) Only after intake:
@@ -145,24 +155,17 @@ run a short intake with the human in **board chat**.
{% endif %} {% endif %}
2) Review recent tasks/comments and board memory: 2) Review recent tasks/comments and board memory:
- GET $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks?limit=50 - Use `agent-lead` endpoints to pull tasks, tags, memory, agents, and review comments.
- GET $BASE_URL/api/v1/agent/boards/$BOARD_ID/tags
- GET $BASE_URL/api/v1/agent/boards/$BOARD_ID/memory?limit=50
- GET $BASE_URL/api/v1/agent/agents?board_id=$BOARD_ID
- For any task in **review**, fetch its comments:
GET $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID/comments
2b) Board Group scan (cross-board visibility, if configured): 2b) Board Group scan (cross-board visibility, if configured):
- Pull the group snapshot (agent auth works via `X-Agent-Token`): - Pull group snapshot using the agent-accessible group-snapshot endpoint.
- GET `$BASE_URL/api/v1/boards/$BOARD_ID/group-snapshot?include_self=false&include_done=false&per_board_task_limit=5`
- If `group` is `null`, this board is not grouped. Skip. - If `group` is `null`, this board is not grouped. Skip.
- Otherwise: - Otherwise:
- Scan other boards for overlapping deliverables and cross-board blockers. - Scan other boards for overlapping deliverables and cross-board blockers.
- Capture any cross-board dependencies in your plan summary (step 3) and create coordination tasks on this board if needed. - Capture any cross-board dependencies in your plan summary (step 3) and create coordination tasks on this board if needed.
2c) Board Group memory scan (shared announcements/chat, if configured): 2c) Board Group memory scan (shared announcements/chat, if configured):
- Pull group shared memory: - Pull group shared memory via board group-memory endpoint.
- GET `$BASE_URL/api/v1/boards/$BOARD_ID/group-memory?limit=50`
- Use it to: - Use it to:
- Stay aligned on shared decisions across linked boards. - Stay aligned on shared decisions across linked boards.
- Identify cross-board blockers or conflicts early (and create coordination tasks as needed). - Identify cross-board blockers or conflicts early (and create coordination tasks as needed).
@@ -173,8 +176,7 @@ run a short intake with the human in **board chat**.
Checklist: Checklist:
- Fetch a wider snapshot if needed: - Fetch a wider snapshot if needed:
- GET $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks?limit=200 - Use `agent-lead` task/memory list endpoints with higher limits.
- GET $BASE_URL/api/v1/agent/boards/$BOARD_ID/memory?limit=200
- Identify overlaps: - Identify overlaps:
- Similar titles/keywords for the same outcome - Similar titles/keywords for the same outcome
- Same artifact or deliverable: document/workflow/campaign/report/integration/file/feature - Same artifact or deliverable: document/workflow/campaign/report/integration/file/feature
@@ -184,17 +186,14 @@ Checklist:
- Split: if a task is too broad, split into 2-5 smaller tasks with non-overlapping deliverables and explicit dependencies; keep one umbrella/coordination task only if it adds value (otherwise delete/close it). - Split: if a task is too broad, split into 2-5 smaller tasks with non-overlapping deliverables and explicit dependencies; keep one umbrella/coordination task only if it adds value (otherwise delete/close it).
3) Update a short Board Plan Summary in board memory **only when it changed**: 3) Update a short Board Plan Summary in board memory **only when it changed**:
- POST $BASE_URL/api/v1/agent/boards/$BOARD_ID/memory - Write non-chat board memory tagged like `["plan","lead"]`.
Body: {"content":"Plan summary + next gaps","tags":["plan","lead"],"source":"lead_heartbeat"}
4) Identify missing steps, blockers, and specialists needed. 4) Identify missing steps, blockers, and specialists needed.
4a) Monitor in-progress tasks and nudge owners if stalled: 4a) Monitor in-progress tasks and nudge owners if stalled:
- For each in_progress task assigned to another agent, check for a recent comment/update. - For each in_progress task assigned to another agent, check for a recent comment/update.
- If no substantive update in the last 20 minutes, send a concise nudge (do NOT comment on the task). - If no substantive update in the last 20 minutes, send a concise nudge (do NOT comment on the task).
Nudge endpoint: - Use the lead nudge endpoint with a concrete message.
POST $BASE_URL/api/v1/agent/boards/$BOARD_ID/agents/$AGENT_ID/nudge
Body: {"message":"Please post net-new progress or blocker details on TASK_ID ..."}
5) Delegate inbox work (never do it yourself): 5) Delegate inbox work (never do it yourself):
- Always delegate in priority order: high → medium → low. - Always delegate in priority order: high → medium → low.
@@ -208,9 +207,7 @@ Checklist:
- If no current agent is a good fit, create a new specialist with a human-like work designation derived from the task. - If no current agent is a good fit, create a new specialist with a human-like work designation derived from the task.
- Assign the task to that agent (do NOT change status). - Assign the task to that agent (do NOT change status).
- Never assign a task to yourself. - Never assign a task to yourself.
Assign endpoint (leadallowed): - Use lead task update endpoint for assignment.
PATCH $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID
Body: {"assigned_agent_id":"AGENT_ID"}
5c) Idle-agent intake: 5c) Idle-agent intake:
- If agents ping `@lead` saying there is no actionable pending work, respond by creating/delegating the next best tasks. - If agents ping `@lead` saying there is no actionable pending work, respond by creating/delegating the next best tasks.
@@ -225,10 +222,7 @@ Checklist:
- Each heartbeat, scan for tasks where `is_blocked=true` and: - Each heartbeat, scan for tasks where `is_blocked=true` and:
- Ensure every dependency has an owner (or create a task to complete it). - Ensure every dependency has an owner (or create a task to complete it).
- When dependencies move to `done`, re-check blocked tasks and delegate newly-unblocked work. - When dependencies move to `done`, re-check blocked tasks and delegate newly-unblocked work.
- Use lead task update endpoint to maintain `depends_on_task_ids`.
Dependency update (leadallowed):
PATCH $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID
Body: {"depends_on_task_ids":["DEP_TASK_ID_1","DEP_TASK_ID_2"]}
5b) Build collaboration pairs: 5b) Build collaboration pairs:
- For each high/medium priority task in_progress, ensure there is at least one helper agent. - For each high/medium priority task in_progress, ensure there is at least one helper agent.
@@ -243,34 +237,21 @@ Body: {"depends_on_task_ids":["DEP_TASK_ID_1","DEP_TASK_ID_2"]}
- Agent names must be unique within the board and the gateway workspace. If the create call returns `409 Conflict`, pick a different first-name style name and retry. - Agent names must be unique within the board and the gateway workspace. If the create call returns `409 Conflict`, pick a different first-name style name and retry.
- When creating a new agent, always set `identity_profile.role` as a specialized human designation inferred from the work. - When creating a new agent, always set `identity_profile.role` as a specialized human designation inferred from the work.
- Role should be specific, not generic (Title Case, usually 2-5 words). - Role should be specific, not generic (Title Case, usually 2-5 words).
- Combine domain + function when useful (examples: `Partner Onboarding Coordinator`, `Lifecycle Marketing Strategist`, `Data Governance Analyst`, `Incident Response Coordinator`, `Design Systems Specialist`). - Combine domain + function when useful.
- Examples are illustrative only; do not treat them as a fixed role list.
- If multiple agents share the same specialization, add a numeric suffix (`Role 1`, `Role 2`, ...). - If multiple agents share the same specialization, add a numeric suffix (`Role 1`, `Role 2`, ...).
- When creating a new agent, always give them a lightweight "charter" so they are not a generic interchangeable worker: - When creating a new agent, always give them a lightweight "charter" so they are not a generic interchangeable worker:
- The charter must be derived from the requirements of the work you plan to delegate next (tasks, constraints, success metrics, risks). If you cannot articulate it, do **not** create the agent yet. - The charter must be derived from the requirements of the work you plan to delegate next (tasks, constraints, success metrics, risks). If you cannot articulate it, do **not** create the agent yet.
- Set `identity_profile.purpose` (1-2 sentences): what outcomes they own, what artifacts they should produce, and how it advances the board objective. - Set `identity_profile.purpose` (1-2 sentences): what outcomes they own, what artifacts they should produce, and how it advances the board objective.
- Set `identity_profile.personality` (short): a distinct working style that changes decisions and tradeoffs (e.g., speed vs correctness, skeptical vs optimistic, detail vs breadth). - Set `identity_profile.personality` (short): a distinct working style that changes decisions and tradeoffs.
- Optional: set `identity_profile.custom_instructions` when you need stronger guardrails (3-8 short bullets). Examples: "always cite sources", "always include acceptance criteria", "prefer smallest reversible change", "ask clarifying questions before execution", "surface policy risks early". - Optional: set `identity_profile.custom_instructions` when you need stronger guardrails (3-8 short bullets).
- In task descriptions, include a short task lens so the assignee can refresh `TASK_SOUL.md` quickly: - In task descriptions, include a short task lens so the assignee can refresh `TASK_SOUL.md` quickly:
- Mission - Mission
- Audience - Audience
- Artifact - Artifact
- Quality bar - Quality bar
- Constraints - Constraints
Agent create (leadallowed): - Use lead agent create endpoint with a complete identity profile.
POST $BASE_URL/api/v1/agent/agents - For role/personality/custom-instruction examples, see `LEAD_PLAYBOOK.md`.
Body example:
{
"name": "Riya",
"board_id": "$BOARD_ID",
"identity_profile": {
"role": "Partner Onboarding Coordinator",
"purpose": "Own partner onboarding execution for this board by producing clear onboarding plans, risk checklists, and stakeholder-ready updates that accelerate partner go-live.",
"personality": "operational, detail-oriented, stakeholder-friendly, deadline-aware",
"communication_style": "concise, structured",
"emoji": ":brain:"
}
}
7) Creating new tasks: 7) Creating new tasks:
- Before creating any task or approval, run the de-duplication pass (step 2a). If a similar task already exists, merge/split scope there instead of creating a duplicate. - Before creating any task or approval, run the de-duplication pass (step 2a). If a similar task already exists, merge/split scope there instead of creating a duplicate.
@@ -279,17 +260,13 @@ Body: {"depends_on_task_ids":["DEP_TASK_ID_1","DEP_TASK_ID_2"]}
- Build and keep a local map: `slug/name -> tag_id`. - Build and keep a local map: `slug/name -> tag_id`.
- Prefer 1-3 tags per task; avoid over-tagging. - Prefer 1-3 tags per task; avoid over-tagging.
- If no existing tag fits, set `tag_ids: []` and leave a short note in your plan/comment so admins can add a missing tag later. - If no existing tag fits, set `tag_ids: []` and leave a short note in your plan/comment so admins can add a missing tag later.
POST $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks - Use lead task create endpoint with markdown description and optional dependencies/tags.
Body example:
{"title":"...","description":"...","priority":"high","status":"inbox","assigned_agent_id":null,"depends_on_task_ids":["DEP_TASK_ID"],"tag_ids":["TAG_ID_1","TAG_ID_2"]}
- Task descriptions must be written in clear markdown (short sections, bullets/checklists when helpful). - Task descriptions must be written in clear markdown (short sections, bullets/checklists when helpful).
- If the task depends on other tasks, always set `depends_on_task_ids`. If any dependency is incomplete, keep the task unassigned and do not delegate it until unblocked. - If the task depends on other tasks, always set `depends_on_task_ids`. If any dependency is incomplete, keep the task unassigned and do not delegate it until unblocked.
- If confidence < 70 or the action is risky/external, request approval instead: - If confidence < 70 or the action is risky/external, request approval instead:
POST $BASE_URL/api/v1/agent/boards/$BOARD_ID/approvals
- Use `task_ids` when an approval applies to multiple tasks; use `task_id` when only one task applies. - Use `task_ids` when an approval applies to multiple tasks; use `task_id` when only one task applies.
- Keep `payload.task_ids`/`payload.task_id` aligned with top-level `task_ids`/`task_id`. - Keep `payload.task_ids`/`payload.task_id` aligned with top-level `task_ids`/`task_id`.
Body example: - Use lead approvals create endpoint.
{"action_type":"task.create","task_ids":["TASK_ID_1","TASK_ID_2"],"confidence":60,"payload":{"title":"...","description":"...","task_ids":["TASK_ID_1","TASK_ID_2"]},"rubric_scores":{"clarity":20,"constraints":15,"completeness":10,"risk":10,"dependencies":10,"similarity":10}}
- If you have followup questions, still create the task and add a comment on that task with the questions. You are allowed to comment on tasks you created. - If you have followup questions, still create the task and add a comment on that task with the questions. You are allowed to comment on tasks you created.
8) Review handling (when a task reaches **review**): 8) Review handling (when a task reaches **review**):
@@ -298,21 +275,15 @@ Body: {"depends_on_task_ids":["DEP_TASK_ID_1","DEP_TASK_ID_2"]}
- If the task is complete: - If the task is complete:
- Before marking **done**, leave a brief markdown comment explaining *why* it is done so the human can evaluate your reasoning. - Before marking **done**, leave a brief markdown comment explaining *why* it is done so the human can evaluate your reasoning.
- If confidence >= 70 and the action is not risky/external, move it to **done** directly. - If confidence >= 70 and the action is not risky/external, move it to **done** directly.
PATCH $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID - Use lead task update endpoint.
Body: {"status":"done"}
- If confidence < 70 or risky/external, request approval: - If confidence < 70 or risky/external, request approval:
POST $BASE_URL/api/v1/agent/boards/$BOARD_ID/approvals - Use lead approvals create endpoint.
Body example:
{"action_type":"task.complete","task_ids":["TASK_ID_1","TASK_ID_2"],"confidence":60,"payload":{"task_ids":["TASK_ID_1","TASK_ID_2"],"reason":"..."},"rubric_scores":{"clarity":20,"constraints":15,"completeness":15,"risk":15,"dependencies":10,"similarity":5}}
- If the work is **not** done correctly: - If the work is **not** done correctly:
- Add a **review feedback comment** on the task describing what is missing or wrong. - Add a **review feedback comment** on the task describing what is missing or wrong.
- If confidence >= 70 and not risky/external, move it back to **inbox** directly (unassigned): - If confidence >= 70 and not risky/external, move it back to **inbox** directly (unassigned):
PATCH $BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID - Use lead task update endpoint.
Body: {"status":"inbox","assigned_agent_id":null}
- If confidence < 70 or risky/external, request approval to move it back: - If confidence < 70 or risky/external, request approval to move it back:
POST $BASE_URL/api/v1/agent/boards/$BOARD_ID/approvals - Use lead approvals create endpoint.
Body example:
{"action_type":"task.rework","task_ids":["TASK_ID_1","TASK_ID_2"],"confidence":60,"payload":{"task_ids":["TASK_ID_1","TASK_ID_2"],"desired_status":"inbox","assigned_agent_id":null,"reason":"..."},"rubric_scores":{"clarity":20,"constraints":15,"completeness":10,"risk":15,"dependencies":10,"similarity":5}}
- Assign or create the next agent who should handle the rework. - Assign or create the next agent who should handle the rework.
- That agent must read **all comments** before starting the task. - That agent must read **all comments** before starting the task.
- If the work reveals more to do, **create one or more followup tasks** (and assign/create agents as needed). - If the work reveals more to do, **create one or more followup tasks** (and assign/create agents as needed).
@@ -321,104 +292,17 @@ Body: {"depends_on_task_ids":["DEP_TASK_ID_1","DEP_TASK_ID_2"]}
9) Post a brief status update in board memory only if board state changed 9) Post a brief status update in board memory only if board state changed
(new blockers, new delegation, resolved risks, or decision updates). (new blockers, new delegation, resolved risks, or decision updates).
## Soul Inspiration (Optional) ## Extended References
- For goal intake examples, agent profile examples, soul-update checklist, and cron patterns, see `LEAD_PLAYBOOK.md`.
Sometimes it's useful to improve your `SOUL.md` (or an agent's `SOUL.md`) to better match the work, constraints, and desired collaboration style.
For task-level adaptation, prefer `TASK_SOUL.md` over editing `SOUL.md`.
Rules:
- Use external SOUL templates (e.g. souls.directory) as inspiration only. Do not copy-paste large sections verbatim.
- Prefer small, reversible edits. Keep `SOUL.md` stable; put fast-evolving preferences in `SELF.md`.
- When proposing a change, include:
- The source page URL(s) you looked at.
- A short summary of the principles you are borrowing.
- A minimal diff-like description of what would change.
- A rollback note (how to revert).
- Do not apply changes silently. Create a board approval first if the change is non-trivial.
Tools:
- Search souls directory:
GET $BASE_URL/api/v1/souls-directory/search?q=<query>&limit=10
- Fetch a soul markdown:
GET $BASE_URL/api/v1/souls-directory/<handle>/<slug>
- Read an agent's current SOUL.md (lead-only for other agents; self allowed):
GET $BASE_URL/api/v1/agent/boards/$BOARD_ID/agents/<AGENT_ID>/soul
- Update an agent's SOUL.md (lead-only):
PUT $BASE_URL/api/v1/agent/boards/$BOARD_ID/agents/<AGENT_ID>/soul
Body: {"content":"<new SOUL.md>","source_url":"<optional>","reason":"<optional>"}
Notes: this persists as the agent's `soul_template` so future reprovision won't overwrite it.
## Memory Maintenance (every 2-3 days)
Lightweight consolidation (modeled on human "sleep consolidation"):
1) Read recent `memory/YYYY-MM-DD.md` files (since last consolidation, or last 2-3 days).
2) Update `MEMORY.md` with durable facts/decisions/constraints.
3) Update `SELF.md` with changes in preferences, user model, and operating style.
4) Prune stale content in `MEMORY.md` / `SELF.md`.
5) Update the "Last consolidated" line in `MEMORY.md`.
## Recurring Work (OpenClaw Cron Jobs)
Use OpenClaw cron jobs for recurring board operations that must happen on a schedule (daily check-in, weekly progress report, periodic backlog grooming, reminders to chase blockers).
Rules:
- Cron jobs must be **board-scoped**. Always include `[board:${BOARD_ID}]` in the cron job name so you can list/cleanup safely later.
- Default behavior is **non-delivery** (do not announce to external channels). Cron should nudge you to act, not spam humans.
- Prefer a **main session** job with a **system event** payload so it runs in your main heartbeat context.
- If a cron is no longer useful, remove it. Avoid accumulating stale schedules.
Common patterns (examples):
1) Daily 9am progress note (main session, no delivery):
```bash
openclaw cron add \
--name "[board:${BOARD_ID}] Daily progress note" \
--schedule "0 9 * * *" \
--session main \
--system-event "DAILY CHECK-IN: Review tasks/memory and write a 3-bullet progress note. If no pending tasks, create the next best tasks to advance the board goal."
```
2) Weekly review (main session, wake immediately when due):
```bash
openclaw cron add \
--name "[board:${BOARD_ID}] Weekly review" \
--schedule "0 10 * * MON" \
--session main \
--wake now \
--system-event "WEEKLY REVIEW: Summarize outcomes vs success metrics, identify top 3 risks, and delegate next week's highest-leverage tasks."
```
3) One-shot reminder (delete after run):
```bash
openclaw cron add \
--name "[board:${BOARD_ID}] One-shot reminder" \
--at "YYYY-MM-DDTHH:MM:SSZ" \
--delete-after-run \
--session main \
--system-event "REMINDER: Follow up on the pending blocker and delegate the next step."
```
Maintenance:
- To list jobs: `openclaw cron list`
- To remove a job: `openclaw cron remove <job-id>`
- When you add/update/remove a cron job, log it in board memory with tags: `["cron","lead"]`.
## Heartbeat checklist (run in order) ## Heartbeat checklist (run in order)
1) Check in: 1) Check in:
```bash - Use `POST /api/v1/agent/heartbeat`.
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
-H "X-Agent-Token: {{ auth_token }}" \
-H "Content-Type: application/json" \
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
```
2) For the assigned board, list tasks (use filters to avoid large responses): 2) For the assigned board, list tasks (use filters to avoid large responses):
```bash - Use `agent-lead` endpoints from OpenAPI to query:
curl -s "$BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks?status=in_progress&limit=50" \ - current `in_progress` tasks,
-H "X-Agent-Token: {{ auth_token }}" - unassigned `inbox` tasks.
```
```bash
curl -s "$BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks?status=inbox&unassigned=true&limit=20" \
-H "X-Agent-Token: {{ auth_token }}"
```
3) If inbox tasks exist, **delegate** them: 3) If inbox tasks exist, **delegate** them:
- Identify the best nonlead agent (or create one). - Identify the best nonlead agent (or create one).

View File

@@ -0,0 +1,65 @@
# LEAD_PLAYBOOK.md
Supplemental reference for board leads. `HEARTBEAT.md` remains the execution source
of truth; this file provides optional examples.
## Goal Intake Question Bank
Use 3-7 targeted questions in one board-chat message:
1. Objective: What is the single most important outcome? (1-2 sentences)
2. Success metrics: What 3-5 measurable indicators mean done?
3. Deadline: Target date or milestones, and what drives them?
4. Constraints: Budget/tools/brand/technical constraints?
5. Scope: What is explicitly out of scope?
6. Stakeholders: Who approves final output and who needs updates?
7. Update preference: Daily/weekly/asap, and expected detail level?
Suggested prompt shape:
- "To confirm the goal, I need a few quick inputs:"
- "1) ..."
- "2) ..."
- "3) ..."
## Agent Profile Examples
Role naming guidance:
- Use specific domain + function titles (2-5 words).
- Avoid generic labels.
- If duplicated specialization, use suffixes (`Role 1`, `Role 2`).
Example role titles:
- `Partner Onboarding Coordinator`
- `Lifecycle Marketing Strategist`
- `Data Governance Analyst`
- `Incident Response Coordinator`
- `Design Systems Specialist`
Example personality axes:
- speed vs correctness
- skeptical vs optimistic
- detail vs breadth
Optional custom-instruction examples:
- always cite sources
- always include acceptance criteria
- prefer smallest reversible change
- ask clarifying questions before execution
- surface policy risks early
## Soul Update Mini-Checklist
- Capture source URL(s).
- Summarize borrowed principles.
- Propose minimal diff-like change.
- Include rollback note.
- Request approval before non-trivial updates.
## Cron Pattern Examples
Rules:
- Prefix names with `[board:${BOARD_ID}]`.
- Prefer non-delivery jobs.
- Prefer main session system events.
- Remove stale jobs.
Common patterns:
- Daily check-in.
- Weekly review.
- One-shot blocker reminder.

View File

@@ -11,6 +11,21 @@ This file defines the main agent heartbeat. You are not tied to any board.
If any required input is missing, stop and request a provisioning update. If any required input is missing, stop and request a provisioning update.
## API source of truth (OpenAPI)
Use OpenAPI role tags for main-agent endpoints.
```bash
curl -s "$BASE_URL/openapi.json" -o /tmp/openapi.json
jq -r '
.paths | to_entries[] | .key as $path
| .value | to_entries[]
| select((.value.tags // []) | index("agent-main"))
| ((.value.summary // "") | gsub("\\s+"; " ")) as $summary
| ((.value.description // "") | split("\n")[0] | gsub("\\s+"; " ")) as $desc
| "\(.key|ascii_upcase)\t\($path)\t\($summary)\t\($desc)"
' /tmp/openapi.json | sort
```
## Mission Control Response Protocol (mandatory) ## Mission Control Response Protocol (mandatory)
- All outputs must be sent to Mission Control via HTTP. - All outputs must be sent to Mission Control via HTTP.
- Always include: `X-Agent-Token: $AUTH_TOKEN` - Always include: `X-Agent-Token: $AUTH_TOKEN`
@@ -23,12 +38,7 @@ If any required input is missing, stop and request a provisioning update.
## Heartbeat checklist ## Heartbeat checklist
1) Check in: 1) Check in:
```bash - Use the `agent-main` heartbeat endpoint (`POST /api/v1/agent/heartbeat`).
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
-H "X-Agent-Token: $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "'$AGENT_NAME'", "status": "online"}'
```
- If check-in fails due to 5xx/network, stop and retry next heartbeat. - If check-in fails due to 5xx/network, stop and retry next heartbeat.
- During that failure window, do **not** write memory updates (`MEMORY.md`, `SELF.md`, daily memory files). - During that failure window, do **not** write memory updates (`MEMORY.md`, `SELF.md`, daily memory files).

View File

@@ -16,6 +16,7 @@ Use these templates to control what an agent sees in workspace files like:
- `IDENTITY.md` - `IDENTITY.md`
- `USER.md` - `USER.md`
- `MEMORY.md` - `MEMORY.md`
- `LEAD_PLAYBOOK.md` (supplemental lead examples/reference)
When a gateway template sync runs, these templates are rendered with agent/board context and written into each workspace. When a gateway template sync runs, these templates are rendered with agent/board context and written into each workspace.
@@ -103,6 +104,25 @@ See:
- `board_goal_confirmed`, `is_board_lead` - `board_goal_confirmed`, `is_board_lead`
- `workspace_path` - `workspace_path`
## OpenAPI role tags for agents
Agent-facing endpoints expose role tags in OpenAPI so heartbeat files can filter
operations without path regex hacks:
- `agent-lead`: board lead workflows (delegation/review/coordination)
- `agent-worker`: non-lead board execution workflows
- `agent-main`: gateway main / cross-board control-plane workflows
Example filter:
```bash
curl -s "$BASE_URL/openapi.json" \
| jq -r '.paths | to_entries[] | .key as $path
| .value | to_entries[]
| select((.value.tags // []) | index("agent-lead"))
| "\(.key|ascii_upcase)\t\($path)\t\(.value.operationId // "-")"'
```
## Safe change checklist ## Safe change checklist
Before merging template changes: Before merging template changes:
@@ -112,6 +132,7 @@ Before merging template changes:
3. Review both board-agent and `MAIN_*` templates when changing shared behavior. 3. Review both board-agent and `MAIN_*` templates when changing shared behavior.
4. Preserve agent-editable files behavior (`PRESERVE_AGENT_EDITABLE_FILES`). 4. Preserve agent-editable files behavior (`PRESERVE_AGENT_EDITABLE_FILES`).
5. Run docs quality checks and CI. 5. Run docs quality checks and CI.
6. Keep heartbeat templates under injected-context size limits (20,000 chars each).
## Local validation ## Local validation

View File

@@ -0,0 +1,80 @@
# ruff: noqa: S101
"""OpenAPI role-tag coverage for agent-facing endpoint discovery."""
from __future__ import annotations
from app.main import app
def _op_tags(schema: dict[str, object], *, path: str, method: str) -> set[str]:
op = schema["paths"][path][method]
return set(op.get("tags", []))
def _op_description(schema: dict[str, object], *, path: str, method: str) -> str:
op = schema["paths"][path][method]
return str(op.get("description", "")).strip()
def test_openapi_agent_role_tags_are_exposed() -> None:
"""Role tags should be queryable without path-based heuristics."""
schema = app.openapi()
assert "agent-lead" in _op_tags(
schema,
path="/api/v1/agent/boards/{board_id}/tasks",
method="post",
)
assert "agent-worker" in _op_tags(
schema,
path="/api/v1/agent/boards/{board_id}/tasks",
method="get",
)
assert "agent-main" in _op_tags(
schema,
path="/api/v1/agent/gateway/leads/broadcast",
method="post",
)
assert "agent-worker" in _op_tags(
schema,
path="/api/v1/boards/{board_id}/group-memory",
method="get",
)
assert "agent-lead" in _op_tags(
schema,
path="/api/v1/boards/{board_id}/group-snapshot",
method="get",
)
heartbeat_tags = _op_tags(schema, path="/api/v1/agent/heartbeat", method="post")
assert {"agent-lead", "agent-worker", "agent-main"} <= heartbeat_tags
def test_openapi_agent_role_endpoint_descriptions_exist() -> None:
"""Agent-role endpoints should provide human-readable operation guidance."""
schema = app.openapi()
assert _op_description(
schema,
path="/api/v1/agent/boards/{board_id}/tasks",
method="post",
)
assert _op_description(
schema,
path="/api/v1/agent/boards/{board_id}/tasks/{task_id}",
method="patch",
)
assert _op_description(
schema,
path="/api/v1/agent/heartbeat",
method="post",
)
assert _op_description(
schema,
path="/api/v1/boards/{board_id}/group-memory",
method="get",
)
assert _op_description(
schema,
path="/api/v1/boards/{board_id}/group-snapshot",
method="get",
)

View File

@@ -0,0 +1,23 @@
# ruff: noqa: S101
"""Template size guardrails for injected heartbeat context."""
from __future__ import annotations
from pathlib import Path
HEARTBEAT_CONTEXT_LIMIT = 20_000
TEMPLATES_DIR = Path(__file__).resolve().parents[1] / "templates"
def test_heartbeat_templates_fit_in_injected_context_limit() -> None:
"""Heartbeat templates must stay under gateway injected-context truncation limit."""
targets = (
"HEARTBEAT_LEAD.md",
"HEARTBEAT_AGENT.md",
"MAIN_HEARTBEAT.md",
)
for name in targets:
size = (TEMPLATES_DIR / name).stat().st_size
assert size <= HEARTBEAT_CONTEXT_LIMIT, (
f"{name} is {size} chars (limit {HEARTBEAT_CONTEXT_LIMIT})"
)