feat: add is_board_lead property to agent and board types
This commit is contained in:
239
backend/app/api/agent.py
Normal file
239
backend/app/api/agent.py
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from app.api import agents as agents_api
|
||||||
|
from app.api import approvals as approvals_api
|
||||||
|
from app.api import board_memory as board_memory_api
|
||||||
|
from app.api import board_onboarding as onboarding_api
|
||||||
|
from app.api import tasks as tasks_api
|
||||||
|
from app.api.deps import ActorContext, get_board_or_404, get_task_or_404
|
||||||
|
from app.core.agent_auth import AgentAuthContext, get_agent_auth_context
|
||||||
|
from app.db.session import get_session
|
||||||
|
from app.models.boards import Board
|
||||||
|
from app.schemas.approvals import ApprovalCreate, ApprovalRead
|
||||||
|
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
|
||||||
|
from app.schemas.board_onboarding import BoardOnboardingRead
|
||||||
|
from app.schemas.boards import BoardRead
|
||||||
|
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskRead, TaskUpdate
|
||||||
|
from app.schemas.agents import AgentCreate, AgentHeartbeatCreate, AgentRead
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/agent", tags=["agent"])
|
||||||
|
|
||||||
|
|
||||||
|
def _actor(agent_ctx: AgentAuthContext) -> ActorContext:
|
||||||
|
return ActorContext(actor_type="agent", agent=agent_ctx.agent)
|
||||||
|
|
||||||
|
|
||||||
|
def _guard_board_access(agent_ctx: AgentAuthContext, board: Board) -> None:
|
||||||
|
if agent_ctx.agent.board_id and agent_ctx.agent.board_id != board.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/boards", response_model=list[BoardRead])
|
||||||
|
def list_boards(
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> list[Board]:
|
||||||
|
if agent_ctx.agent.board_id:
|
||||||
|
board = session.get(Board, agent_ctx.agent.board_id)
|
||||||
|
return [board] if board else []
|
||||||
|
return list(session.exec(select(Board)))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/boards/{board_id}", response_model=BoardRead)
|
||||||
|
def get_board(
|
||||||
|
board: Board = Depends(get_board_or_404),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> Board:
|
||||||
|
_guard_board_access(agent_ctx, board)
|
||||||
|
return board
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/boards/{board_id}/tasks", response_model=list[TaskRead])
|
||||||
|
def list_tasks(
|
||||||
|
status_filter: str | None = Query(default=None, alias="status"),
|
||||||
|
assigned_agent_id: UUID | None = None,
|
||||||
|
unassigned: bool | None = None,
|
||||||
|
limit: int | None = Query(default=None, ge=1, le=200),
|
||||||
|
board: Board = Depends(get_board_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> list[TaskRead]:
|
||||||
|
_guard_board_access(agent_ctx, board)
|
||||||
|
return tasks_api.list_tasks(
|
||||||
|
status_filter=status_filter,
|
||||||
|
assigned_agent_id=assigned_agent_id,
|
||||||
|
unassigned=unassigned,
|
||||||
|
limit=limit,
|
||||||
|
board=board,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/boards/{board_id}/tasks/{task_id}", response_model=TaskRead)
|
||||||
|
def update_task(
|
||||||
|
payload: TaskUpdate,
|
||||||
|
task=Depends(get_task_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> TaskRead:
|
||||||
|
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
return tasks_api.update_task(
|
||||||
|
payload=payload,
|
||||||
|
task=task,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/boards/{board_id}/tasks/{task_id}/comments", response_model=list[TaskCommentRead])
|
||||||
|
def list_task_comments(
|
||||||
|
task=Depends(get_task_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> list[TaskCommentRead]:
|
||||||
|
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
return tasks_api.list_task_comments(
|
||||||
|
task=task,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/boards/{board_id}/tasks/{task_id}/comments", response_model=TaskCommentRead)
|
||||||
|
def create_task_comment(
|
||||||
|
payload: TaskCommentCreate,
|
||||||
|
task=Depends(get_task_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> TaskCommentRead:
|
||||||
|
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
return tasks_api.create_task_comment(
|
||||||
|
payload=payload,
|
||||||
|
task=task,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/boards/{board_id}/memory", response_model=list[BoardMemoryRead])
|
||||||
|
def list_board_memory(
|
||||||
|
limit: int = Query(default=50, ge=1, le=200),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
board=Depends(get_board_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> list[BoardMemoryRead]:
|
||||||
|
_guard_board_access(agent_ctx, board)
|
||||||
|
return board_memory_api.list_board_memory(
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
board=board,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead)
|
||||||
|
def create_board_memory(
|
||||||
|
payload: BoardMemoryCreate,
|
||||||
|
board=Depends(get_board_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> BoardMemoryRead:
|
||||||
|
_guard_board_access(agent_ctx, board)
|
||||||
|
return board_memory_api.create_board_memory(
|
||||||
|
payload=payload,
|
||||||
|
board=board,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/boards/{board_id}/approvals", response_model=list[ApprovalRead])
|
||||||
|
def list_approvals(
|
||||||
|
status_filter: str | None = Query(default=None, alias="status"),
|
||||||
|
board=Depends(get_board_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> list[ApprovalRead]:
|
||||||
|
_guard_board_access(agent_ctx, board)
|
||||||
|
return approvals_api.list_approvals(
|
||||||
|
status_filter=status_filter,
|
||||||
|
board=board,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/boards/{board_id}/approvals", response_model=ApprovalRead)
|
||||||
|
def create_approval(
|
||||||
|
payload: ApprovalCreate,
|
||||||
|
board=Depends(get_board_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> ApprovalRead:
|
||||||
|
_guard_board_access(agent_ctx, board)
|
||||||
|
return approvals_api.create_approval(
|
||||||
|
payload=payload,
|
||||||
|
board=board,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/boards/{board_id}/onboarding", response_model=BoardOnboardingRead)
|
||||||
|
def update_onboarding(
|
||||||
|
payload: dict[str, object],
|
||||||
|
board: Board = Depends(get_board_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> BoardOnboardingRead:
|
||||||
|
_guard_board_access(agent_ctx, board)
|
||||||
|
return onboarding_api.agent_onboarding_update(
|
||||||
|
payload=payload,
|
||||||
|
board=board,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/agents", response_model=AgentRead)
|
||||||
|
def create_agent(
|
||||||
|
payload: AgentCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> AgentRead:
|
||||||
|
if not agent_ctx.agent.is_board_lead:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
if not agent_ctx.agent.board_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
payload = AgentCreate(**{**payload.model_dump(), "board_id": agent_ctx.agent.board_id})
|
||||||
|
return agents_api.create_agent(
|
||||||
|
payload=payload,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/heartbeat", response_model=AgentRead)
|
||||||
|
async def agent_heartbeat(
|
||||||
|
payload: AgentHeartbeatCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||||
|
) -> AgentRead:
|
||||||
|
if agent_ctx.agent.name != payload.name:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
return await agents_api.heartbeat_or_create_agent( # type: ignore[attr-defined]
|
||||||
|
payload=payload,
|
||||||
|
session=session,
|
||||||
|
actor=_actor(agent_ctx),
|
||||||
|
)
|
||||||
@@ -301,6 +301,7 @@ def get_agent(
|
|||||||
async def update_agent(
|
async def update_agent(
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
payload: AgentUpdate,
|
payload: AgentUpdate,
|
||||||
|
force: bool = False,
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
auth: AuthContext = Depends(require_admin_auth),
|
auth: AuthContext = Depends(require_admin_auth),
|
||||||
) -> Agent:
|
) -> Agent:
|
||||||
@@ -321,7 +322,7 @@ async def update_agent(
|
|||||||
updates["identity_profile"] = _normalize_identity_profile(
|
updates["identity_profile"] = _normalize_identity_profile(
|
||||||
updates.get("identity_profile")
|
updates.get("identity_profile")
|
||||||
)
|
)
|
||||||
if not updates:
|
if not updates and not force:
|
||||||
return _with_computed_status(agent)
|
return _with_computed_status(agent)
|
||||||
if "board_id" in updates:
|
if "board_id" in updates:
|
||||||
_require_board(session, updates["board_id"])
|
_require_board(session, updates["board_id"])
|
||||||
@@ -380,9 +381,17 @@ async def update_agent(
|
|||||||
except OpenClawGatewayError as exc:
|
except OpenClawGatewayError as exc:
|
||||||
_record_instruction_failure(session, agent, str(exc), "update")
|
_record_instruction_failure(session, agent, str(exc), "update")
|
||||||
session.commit()
|
session.commit()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail=f"Gateway update failed: {exc}",
|
||||||
|
) from exc
|
||||||
except Exception as exc: # pragma: no cover - unexpected provisioning errors
|
except Exception as exc: # pragma: no cover - unexpected provisioning errors
|
||||||
_record_instruction_failure(session, agent, str(exc), "update")
|
_record_instruction_failure(session, agent, str(exc), "update")
|
||||||
session.commit()
|
session.commit()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Unexpected error updating agent provisioning.",
|
||||||
|
) from exc
|
||||||
return _with_computed_status(agent)
|
return _with_computed_status(agent)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,16 @@ from datetime import datetime
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
import logging
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from app.api.deps import get_board_or_404, require_admin_auth
|
from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent
|
||||||
from app.core.agent_tokens import generate_agent_token, hash_agent_token
|
from app.core.agent_tokens import generate_agent_token, hash_agent_token
|
||||||
from app.core.auth import AuthContext
|
from app.core.auth import AuthContext
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||||
from app.integrations.openclaw_gateway import (
|
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||||
OpenClawGatewayError,
|
|
||||||
ensure_session,
|
|
||||||
get_chat_history,
|
|
||||||
send_message,
|
|
||||||
)
|
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.board_onboarding import BoardOnboardingSession
|
from app.models.board_onboarding import BoardOnboardingSession
|
||||||
from app.models.boards import Board
|
from app.models.boards import Board
|
||||||
@@ -34,59 +30,8 @@ from app.schemas.boards import BoardRead
|
|||||||
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_agent
|
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_agent
|
||||||
|
|
||||||
router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboarding"])
|
router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboarding"])
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _extract_json(text: str) -> dict[str, object] | None:
|
|
||||||
try:
|
|
||||||
return json.loads(text.strip())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
|
|
||||||
if match:
|
|
||||||
try:
|
|
||||||
return json.loads(match.group(1).strip())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
first = text.find("{")
|
|
||||||
last = text.rfind("}")
|
|
||||||
if first != -1 and last > first:
|
|
||||||
try:
|
|
||||||
return json.loads(text[first : last + 1])
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_text(content: object) -> str | None:
|
|
||||||
if isinstance(content, str):
|
|
||||||
return content
|
|
||||||
if isinstance(content, list):
|
|
||||||
for entry in content:
|
|
||||||
if isinstance(entry, dict) and entry.get("type") == "text":
|
|
||||||
text = entry.get("text")
|
|
||||||
if isinstance(text, str):
|
|
||||||
return text
|
|
||||||
if isinstance(content, dict):
|
|
||||||
text = content.get("text")
|
|
||||||
if isinstance(text, str):
|
|
||||||
return text
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_assistant_messages(history: object) -> list[str]:
|
|
||||||
messages: list[str] = []
|
|
||||||
if isinstance(history, dict):
|
|
||||||
history = history.get("messages")
|
|
||||||
if not isinstance(history, list):
|
|
||||||
return messages
|
|
||||||
for msg in history:
|
|
||||||
if not isinstance(msg, dict):
|
|
||||||
continue
|
|
||||||
if msg.get("role") != "assistant":
|
|
||||||
continue
|
|
||||||
text = _extract_text(msg.get("content"))
|
|
||||||
if text:
|
|
||||||
messages.append(text)
|
|
||||||
return messages
|
|
||||||
|
|
||||||
|
|
||||||
def _gateway_config(session: Session, board: Board) -> tuple[Gateway, GatewayClientConfig]:
|
def _gateway_config(session: Session, board: Board) -> tuple[Gateway, GatewayClientConfig]:
|
||||||
@@ -104,7 +49,11 @@ def _build_session_key(agent_name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _lead_agent_name(board: Board) -> str:
|
def _lead_agent_name(board: Board) -> str:
|
||||||
return f"{board.name} Lead"
|
return "Lead Agent"
|
||||||
|
|
||||||
|
|
||||||
|
def _lead_session_key(board: Board) -> str:
|
||||||
|
return f"agent:lead-{board.id}:main"
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_lead_agent(
|
async def _ensure_lead_agent(
|
||||||
@@ -120,6 +69,11 @@ async def _ensure_lead_agent(
|
|||||||
.where(Agent.is_board_lead.is_(True))
|
.where(Agent.is_board_lead.is_(True))
|
||||||
).first()
|
).first()
|
||||||
if existing:
|
if existing:
|
||||||
|
if existing.name != _lead_agent_name(board):
|
||||||
|
existing.name = _lead_agent_name(board)
|
||||||
|
session.add(existing)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(existing)
|
||||||
return existing
|
return existing
|
||||||
|
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
@@ -131,14 +85,14 @@ async def _ensure_lead_agent(
|
|||||||
identity_profile={
|
identity_profile={
|
||||||
"role": "Board Lead",
|
"role": "Board Lead",
|
||||||
"communication_style": "direct, concise, practical",
|
"communication_style": "direct, concise, practical",
|
||||||
"emoji": ":compass:",
|
"emoji": ":gear:",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
raw_token = generate_agent_token()
|
raw_token = generate_agent_token()
|
||||||
agent.agent_token_hash = hash_agent_token(raw_token)
|
agent.agent_token_hash = hash_agent_token(raw_token)
|
||||||
agent.provision_requested_at = datetime.utcnow()
|
agent.provision_requested_at = datetime.utcnow()
|
||||||
agent.provision_action = "provision"
|
agent.provision_action = "provision"
|
||||||
agent.openclaw_session_id = _build_session_key(agent.name)
|
agent.openclaw_session_id = _lead_session_key(board)
|
||||||
session.add(agent)
|
session.add(agent)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(agent)
|
session.refresh(agent)
|
||||||
@@ -200,17 +154,28 @@ async def start_onboarding(
|
|||||||
"BOARD ONBOARDING REQUEST\n\n"
|
"BOARD ONBOARDING REQUEST\n\n"
|
||||||
f"Board Name: {board.name}\n"
|
f"Board Name: {board.name}\n"
|
||||||
"You are the main agent. Ask the user 3-6 focused questions to clarify their goal.\n"
|
"You are the main agent. Ask the user 3-6 focused questions to clarify their goal.\n"
|
||||||
"Only respond in OpenClaw chat with onboarding JSON. All other outputs must be sent to Mission Control via API.\n"
|
"Do NOT respond in OpenClaw chat.\n"
|
||||||
|
"All onboarding responses MUST be sent to Mission Control via API.\n"
|
||||||
f"Mission Control base URL: {base_url}\n"
|
f"Mission Control base URL: {base_url}\n"
|
||||||
"Use the AUTH_TOKEN from MAIN_USER.md or MAIN_TOOLS.md and pass it as X-Agent-Token.\n"
|
"Use the AUTH_TOKEN from USER.md or TOOLS.md and pass it as X-Agent-Token.\n"
|
||||||
"Example API call (for non-onboarding updates):\n"
|
"Onboarding response endpoint:\n"
|
||||||
f"curl -s -X POST \"{base_url}/api/v1/boards/{board.id}/memory\" "
|
f"POST {base_url}/api/v1/agent/boards/{board.id}/onboarding\n"
|
||||||
|
"QUESTION example (send JSON body exactly as shown):\n"
|
||||||
|
f"curl -s -X POST \"{base_url}/api/v1/agent/boards/{board.id}/onboarding\" "
|
||||||
"-H \"X-Agent-Token: $AUTH_TOKEN\" "
|
"-H \"X-Agent-Token: $AUTH_TOKEN\" "
|
||||||
"-H \"Content-Type: application/json\" "
|
"-H \"Content-Type: application/json\" "
|
||||||
"-d '{\"content\":\"Onboarding update...\",\"tags\":[\"onboarding\"],\"source\":\"main_agent\"}'\n"
|
"-d '{\"question\":\"...\",\"options\":[{\"id\":\"1\",\"label\":\"...\"},{\"id\":\"2\",\"label\":\"...\"}]}'\n"
|
||||||
"Return questions as JSON: {\"question\": \"...\", \"options\": [...]}.\n"
|
"COMPLETION example (send JSON body exactly as shown):\n"
|
||||||
"When you have enough info, return JSON: {\"status\": \"complete\", \"board_type\": \"goal\"|\"general\", "
|
f"curl -s -X POST \"{base_url}/api/v1/agent/boards/{board.id}/onboarding\" "
|
||||||
"\"objective\": \"...\", \"success_metrics\": {...}, \"target_date\": \"YYYY-MM-DD\"}."
|
"-H \"X-Agent-Token: $AUTH_TOKEN\" "
|
||||||
|
"-H \"Content-Type: application/json\" "
|
||||||
|
"-d '{\"status\":\"complete\",\"board_type\":\"goal\",\"objective\":\"...\",\"success_metrics\":{...},\"target_date\":\"YYYY-MM-DD\"}'\n"
|
||||||
|
"QUESTION FORMAT (one question per response, no arrays, no markdown, no extra text):\n"
|
||||||
|
"{\"question\":\"...\",\"options\":[{\"id\":\"1\",\"label\":\"...\"},{\"id\":\"2\",\"label\":\"...\"}]}\n"
|
||||||
|
"Do NOT wrap questions in a list. Do NOT add commentary.\n"
|
||||||
|
"When you have enough info, return JSON ONLY (via API):\n"
|
||||||
|
"{\"status\":\"complete\",\"board_type\":\"goal\"|\"general\",\"objective\":\"...\","
|
||||||
|
"\"success_metrics\":{...},\"target_date\":\"YYYY-MM-DD\"}."
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -251,7 +216,7 @@ async def answer_onboarding(
|
|||||||
if payload.other_text:
|
if payload.other_text:
|
||||||
answer_text = f"{payload.answer}: {payload.other_text}"
|
answer_text = f"{payload.answer}: {payload.other_text}"
|
||||||
|
|
||||||
messages = onboarding.messages or []
|
messages = list(onboarding.messages or [])
|
||||||
messages.append(
|
messages.append(
|
||||||
{"role": "user", "content": answer_text, "timestamp": datetime.utcnow().isoformat()}
|
{"role": "user", "content": answer_text, "timestamp": datetime.utcnow().isoformat()}
|
||||||
)
|
)
|
||||||
@@ -261,21 +226,9 @@ async def answer_onboarding(
|
|||||||
await send_message(
|
await send_message(
|
||||||
answer_text, session_key=onboarding.session_key, config=config, deliver=False
|
answer_text, session_key=onboarding.session_key, config=config, deliver=False
|
||||||
)
|
)
|
||||||
history = await get_chat_history(onboarding.session_key, config=config)
|
|
||||||
except OpenClawGatewayError as exc:
|
except OpenClawGatewayError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
||||||
|
|
||||||
assistant_messages = _get_assistant_messages(history)
|
|
||||||
if assistant_messages:
|
|
||||||
last = assistant_messages[-1]
|
|
||||||
messages.append(
|
|
||||||
{"role": "assistant", "content": last, "timestamp": datetime.utcnow().isoformat()}
|
|
||||||
)
|
|
||||||
parsed = _extract_json(last)
|
|
||||||
if parsed and parsed.get("status") == "complete":
|
|
||||||
onboarding.draft_goal = parsed
|
|
||||||
onboarding.status = "completed"
|
|
||||||
|
|
||||||
onboarding.messages = messages
|
onboarding.messages = messages
|
||||||
onboarding.updated_at = datetime.utcnow()
|
onboarding.updated_at = datetime.utcnow()
|
||||||
session.add(onboarding)
|
session.add(onboarding)
|
||||||
@@ -284,6 +237,70 @@ async def answer_onboarding(
|
|||||||
return onboarding
|
return onboarding
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/agent", response_model=BoardOnboardingRead)
|
||||||
|
def agent_onboarding_update(
|
||||||
|
payload: dict[str, object],
|
||||||
|
board: Board = Depends(get_board_or_404),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
actor: ActorContext = Depends(require_admin_or_agent),
|
||||||
|
) -> BoardOnboardingSession:
|
||||||
|
if actor.actor_type != "agent" or actor.agent is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
agent = actor.agent
|
||||||
|
if agent.board_id is not None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
if board.gateway_id:
|
||||||
|
gateway = session.get(Gateway, board.gateway_id)
|
||||||
|
if gateway and gateway.main_session_key and agent.openclaw_session_id:
|
||||||
|
if agent.openclaw_session_id != gateway.main_session_key:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
onboarding = session.exec(
|
||||||
|
select(BoardOnboardingSession)
|
||||||
|
.where(BoardOnboardingSession.board_id == board.id)
|
||||||
|
.order_by(BoardOnboardingSession.created_at.desc())
|
||||||
|
).first()
|
||||||
|
if onboarding is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
if onboarding.status == "confirmed":
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
|
messages = list(onboarding.messages or [])
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
payload_text = json.dumps(payload)
|
||||||
|
logger.info(
|
||||||
|
"onboarding.agent.update board_id=%s agent_id=%s payload=%s",
|
||||||
|
board.id,
|
||||||
|
agent.id,
|
||||||
|
payload_text,
|
||||||
|
)
|
||||||
|
payload_status = payload.get("status")
|
||||||
|
if payload_status == "complete":
|
||||||
|
onboarding.draft_goal = payload
|
||||||
|
onboarding.status = "completed"
|
||||||
|
messages.append({"role": "assistant", "content": payload_text, "timestamp": now})
|
||||||
|
else:
|
||||||
|
question = payload.get("question")
|
||||||
|
options = payload.get("options")
|
||||||
|
if not isinstance(question, str) or not isinstance(options, list):
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||||
|
messages.append({"role": "assistant", "content": payload_text, "timestamp": now})
|
||||||
|
|
||||||
|
onboarding.messages = messages
|
||||||
|
onboarding.updated_at = datetime.utcnow()
|
||||||
|
session.add(onboarding)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(onboarding)
|
||||||
|
logger.info(
|
||||||
|
"onboarding.agent.update stored board_id=%s messages_count=%s status=%s",
|
||||||
|
board.id,
|
||||||
|
len(onboarding.messages or []),
|
||||||
|
onboarding.status,
|
||||||
|
)
|
||||||
|
return onboarding
|
||||||
|
|
||||||
|
|
||||||
@router.post("/confirm", response_model=BoardRead)
|
@router.post("/confirm", response_model=BoardRead)
|
||||||
async def confirm_onboarding(
|
async def confirm_onboarding(
|
||||||
payload: BoardOnboardingConfirm,
|
payload: BoardOnboardingConfirm,
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ async def _ensure_main_agent(
|
|||||||
await send_message(
|
await send_message(
|
||||||
(
|
(
|
||||||
f"Hello {agent.name}. Your gateway provisioning was updated.\n\n"
|
f"Hello {agent.name}. Your gateway provisioning was updated.\n\n"
|
||||||
"Please re-read MAIN_AGENTS.md, MAIN_USER.md, MAIN_HEARTBEAT.md, and MAIN_TOOLS.md. "
|
"Please re-read AGENTS.md, USER.md, HEARTBEAT.md, and TOOLS.md. "
|
||||||
"If BOOTSTRAP.md exists, run it once then delete it. Begin heartbeats after startup."
|
"If BOOTSTRAP.md exists, run it once then delete it. Begin heartbeats after startup."
|
||||||
),
|
),
|
||||||
session_key=gateway.main_session_key,
|
session_key=gateway.main_session_key,
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import Depends, Header, HTTPException, status
|
from fastapi import Depends, Header, HTTPException, Request, status
|
||||||
|
import logging
|
||||||
from sqlmodel import Session, col, select
|
from sqlmodel import Session, col, select
|
||||||
|
|
||||||
from app.core.agent_tokens import verify_agent_token
|
from app.core.agent_tokens import verify_agent_token
|
||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentAuthContext:
|
class AgentAuthContext:
|
||||||
@@ -25,25 +28,78 @@ def _find_agent_for_token(session: Session, token: str) -> Agent | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_agent_token(
|
||||||
|
agent_token: str | None,
|
||||||
|
authorization: str | None,
|
||||||
|
*,
|
||||||
|
accept_authorization: bool = True,
|
||||||
|
) -> str | None:
|
||||||
|
if agent_token:
|
||||||
|
return agent_token
|
||||||
|
if not accept_authorization:
|
||||||
|
return None
|
||||||
|
if not authorization:
|
||||||
|
return None
|
||||||
|
value = authorization.strip()
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
if value.lower().startswith("bearer "):
|
||||||
|
return value.split(" ", 1)[1].strip() or None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_agent_auth_context(
|
def get_agent_auth_context(
|
||||||
|
request: Request,
|
||||||
agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
|
agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
|
||||||
|
authorization: str | None = Header(default=None, alias="Authorization"),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> AgentAuthContext:
|
) -> AgentAuthContext:
|
||||||
if not agent_token:
|
resolved = _resolve_agent_token(agent_token, authorization, accept_authorization=True)
|
||||||
|
if not resolved:
|
||||||
|
logger.warning(
|
||||||
|
"agent auth missing token path=%s x_agent=%s authorization=%s",
|
||||||
|
request.url.path,
|
||||||
|
bool(agent_token),
|
||||||
|
bool(authorization),
|
||||||
|
)
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
agent = _find_agent_for_token(session, agent_token)
|
agent = _find_agent_for_token(session, resolved)
|
||||||
if agent is None:
|
if agent is None:
|
||||||
|
logger.warning(
|
||||||
|
"agent auth invalid token path=%s token_prefix=%s",
|
||||||
|
request.url.path,
|
||||||
|
resolved[:6],
|
||||||
|
)
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
return AgentAuthContext(actor_type="agent", agent=agent)
|
return AgentAuthContext(actor_type="agent", agent=agent)
|
||||||
|
|
||||||
|
|
||||||
def get_agent_auth_context_optional(
|
def get_agent_auth_context_optional(
|
||||||
|
request: Request,
|
||||||
agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
|
agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
|
||||||
|
authorization: str | None = Header(default=None, alias="Authorization"),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> AgentAuthContext | None:
|
) -> AgentAuthContext | None:
|
||||||
if not agent_token:
|
resolved = _resolve_agent_token(
|
||||||
|
agent_token,
|
||||||
|
authorization,
|
||||||
|
accept_authorization=False,
|
||||||
|
)
|
||||||
|
if not resolved:
|
||||||
|
if agent_token:
|
||||||
|
logger.warning(
|
||||||
|
"agent auth optional missing token path=%s x_agent=%s authorization=%s",
|
||||||
|
request.url.path,
|
||||||
|
bool(agent_token),
|
||||||
|
bool(authorization),
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
agent = _find_agent_for_token(session, agent_token)
|
agent = _find_agent_for_token(session, resolved)
|
||||||
if agent is None:
|
if agent is None:
|
||||||
|
logger.warning(
|
||||||
|
"agent auth optional invalid token path=%s token_prefix=%s",
|
||||||
|
request.url.path,
|
||||||
|
resolved[:6],
|
||||||
|
)
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
return AgentAuthContext(actor_type="agent", agent=agent)
|
return AgentAuthContext(actor_type="agent", agent=agent)
|
||||||
|
|||||||
@@ -112,17 +112,17 @@ async def get_auth_context_optional(
|
|||||||
clerk_credentials = await guard(request)
|
clerk_credentials = await guard(request)
|
||||||
except (RuntimeError, ValueError) as exc:
|
except (RuntimeError, ValueError) as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) from exc
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) from exc
|
||||||
except HTTPException as exc:
|
except HTTPException:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from exc
|
return None
|
||||||
|
|
||||||
auth_data = _resolve_clerk_auth(request, clerk_credentials)
|
auth_data = _resolve_clerk_auth(request, clerk_credentials)
|
||||||
try:
|
try:
|
||||||
clerk_user_id = _parse_subject(auth_data)
|
clerk_user_id = _parse_subject(auth_data)
|
||||||
except ValidationError as exc:
|
except ValidationError:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from exc
|
return None
|
||||||
|
|
||||||
if not clerk_user_id:
|
if not clerk_user_id:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
return None
|
||||||
|
|
||||||
user = session.exec(select(User).where(User.clerk_user_id == clerk_user_id)).first()
|
user = session.exec(select(User).where(User.clerk_user_id == clerk_user_id)).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.api.activity import router as activity_router
|
from app.api.activity import router as activity_router
|
||||||
|
from app.api.agent import router as agent_router
|
||||||
from app.api.agents import router as agents_router
|
from app.api.agents import router as agents_router
|
||||||
from app.api.approvals import router as approvals_router
|
from app.api.approvals import router as approvals_router
|
||||||
from app.api.auth import router as auth_router
|
from app.api.auth import router as auth_router
|
||||||
@@ -56,6 +57,7 @@ def readyz() -> dict[str, bool]:
|
|||||||
|
|
||||||
api_v1 = APIRouter(prefix="/api/v1")
|
api_v1 = APIRouter(prefix="/api/v1")
|
||||||
api_v1.include_router(auth_router)
|
api_v1.include_router(auth_router)
|
||||||
|
api_v1.include_router(agent_router)
|
||||||
api_v1.include_router(agents_router)
|
api_v1.include_router(agents_router)
|
||||||
api_v1.include_router(activity_router)
|
api_v1.include_router(activity_router)
|
||||||
api_v1.include_router(gateway_router)
|
api_v1.include_router(gateway_router)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class AgentUpdate(SQLModel):
|
|||||||
|
|
||||||
class AgentRead(AgentBase):
|
class AgentRead(AgentBase):
|
||||||
id: UUID
|
id: UUID
|
||||||
|
is_board_lead: bool = False
|
||||||
openclaw_session_id: str | None = None
|
openclaw_session_id: str | None = None
|
||||||
last_seen_at: datetime | None
|
last_seen_at: datetime | None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ export default function EditAgentPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
const response = await fetch(`${apiBase}/api/v1/agents/${agentId}`, {
|
const response = await fetch(
|
||||||
|
`${apiBase}/api/v1/agents/${agentId}?force=true`,
|
||||||
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -217,7 +219,8 @@ export default function EditAgentPage() {
|
|||||||
identity_profile: normalizeIdentityProfile(identityProfile),
|
identity_profile: normalizeIdentityProfile(identityProfile),
|
||||||
soul_template: soulTemplate.trim() || null,
|
soul_template: soulTemplate.trim() || null,
|
||||||
}),
|
}),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Unable to update agent.");
|
throw new Error("Unable to update agent.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Agent = {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
board_id?: string | null;
|
board_id?: string | null;
|
||||||
|
is_board_lead?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Board = {
|
type Board = {
|
||||||
@@ -106,6 +107,7 @@ export default function AgentDetailPage() {
|
|||||||
return boards.find((board) => board.id === agent.board_id) ?? null;
|
return boards.find((board) => board.id === agent.board_id) ?? null;
|
||||||
}, [boards, agent?.board_id]);
|
}, [boards, agent?.board_id]);
|
||||||
|
|
||||||
|
|
||||||
const loadAgent = async () => {
|
const loadAgent = async () => {
|
||||||
if (!isSignedIn || !agentId) return;
|
if (!isSignedIn || !agentId) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Agent = {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
board_id?: string | null;
|
board_id?: string | null;
|
||||||
|
is_board_lead?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Board = {
|
type Board = {
|
||||||
@@ -138,6 +139,7 @@ export default function AgentsPage() {
|
|||||||
|
|
||||||
const sortedAgents = useMemo(() => [...agents], [agents]);
|
const sortedAgents = useMemo(() => [...agents], [agents]);
|
||||||
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
deleteMutation.mutate(deleteTarget);
|
deleteMutation.mutate(deleteTarget);
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import { useParams, useRouter } from "next/navigation";
|
|||||||
|
|
||||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||||
|
|
||||||
|
import { BoardApprovalsPanel } from "@/components/BoardApprovalsPanel";
|
||||||
|
import { BoardGoalPanel } from "@/components/BoardGoalPanel";
|
||||||
|
import { BoardOnboardingChat } from "@/components/BoardOnboardingChat";
|
||||||
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
||||||
import { DashboardShell } from "@/components/templates/DashboardShell";
|
import { DashboardShell } from "@/components/templates/DashboardShell";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -74,6 +78,7 @@ export default function EditBoardPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [metricsError, setMetricsError] = useState<string | null>(null);
|
const [metricsError, setMetricsError] = useState<string | null>(null);
|
||||||
|
const [isOnboardingOpen, setIsOnboardingOpen] = useState(false);
|
||||||
|
|
||||||
const isFormReady = Boolean(name.trim() && gatewayId);
|
const isFormReady = Boolean(name.trim() && gatewayId);
|
||||||
|
|
||||||
@@ -123,6 +128,17 @@ export default function EditBoardPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnboardingConfirmed = (updated: Board) => {
|
||||||
|
setBoard(updated);
|
||||||
|
setBoardType(updated.board_type ?? "goal");
|
||||||
|
setObjective(updated.objective ?? "");
|
||||||
|
setSuccessMetrics(
|
||||||
|
updated.success_metrics ? JSON.stringify(updated.success_metrics, null, 2) : ""
|
||||||
|
);
|
||||||
|
setTargetDate(toDateInput(updated.target_date));
|
||||||
|
setIsOnboardingOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadBoard();
|
loadBoard();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -199,6 +215,7 @@ export default function EditBoardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<DashboardShell>
|
<DashboardShell>
|
||||||
<SignedOut>
|
<SignedOut>
|
||||||
<div className="col-span-2 flex min-h-[calc(100vh-64px)] items-center justify-center bg-slate-50 p-10 text-center">
|
<div className="col-span-2 flex min-h-[calc(100vh-64px)] items-center justify-center bg-slate-50 p-10 text-center">
|
||||||
@@ -229,6 +246,12 @@ export default function EditBoardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
|
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<BoardGoalPanel
|
||||||
|
board={board}
|
||||||
|
onStartOnboarding={() => setIsOnboardingOpen(true)}
|
||||||
|
/>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
|
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
|
||||||
@@ -347,8 +370,28 @@ export default function EditBoardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{boardId ? <BoardApprovalsPanel boardId={boardId} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</SignedIn>
|
</SignedIn>
|
||||||
</DashboardShell>
|
</DashboardShell>
|
||||||
|
<Dialog open={isOnboardingOpen} onOpenChange={setIsOnboardingOpen}>
|
||||||
|
<DialogContent aria-label="Board onboarding">
|
||||||
|
{boardId ? (
|
||||||
|
<BoardOnboardingChat
|
||||||
|
boardId={boardId}
|
||||||
|
onConfirmed={handleOnboardingConfirmed}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
|
||||||
|
Unable to start onboarding.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
import { BoardApprovalsPanel } from "@/components/BoardApprovalsPanel";
|
|
||||||
import { BoardGoalPanel } from "@/components/BoardGoalPanel";
|
|
||||||
import { BoardOnboardingChat } from "@/components/BoardOnboardingChat";
|
|
||||||
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
||||||
import { TaskBoard } from "@/components/organisms/TaskBoard";
|
import { TaskBoard } from "@/components/organisms/TaskBoard";
|
||||||
import { DashboardShell } from "@/components/templates/DashboardShell";
|
import { DashboardShell } from "@/components/templates/DashboardShell";
|
||||||
@@ -62,6 +59,10 @@ type Agent = {
|
|||||||
name: string;
|
name: string;
|
||||||
status: string;
|
status: string;
|
||||||
board_id?: string | null;
|
board_id?: string | null;
|
||||||
|
is_board_lead?: boolean;
|
||||||
|
identity_profile?: {
|
||||||
|
emoji?: string | null;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TaskComment = {
|
type TaskComment = {
|
||||||
@@ -80,6 +81,19 @@ const priorities = [
|
|||||||
{ value: "high", label: "High" },
|
{ value: "high", label: "High" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const EMOJI_GLYPHS: Record<string, string> = {
|
||||||
|
":gear:": "⚙️",
|
||||||
|
":sparkles:": "✨",
|
||||||
|
":rocket:": "🚀",
|
||||||
|
":megaphone:": "📣",
|
||||||
|
":chart_with_upwards_trend:": "📈",
|
||||||
|
":bulb:": "💡",
|
||||||
|
":wrench:": "🔧",
|
||||||
|
":shield:": "🛡️",
|
||||||
|
":memo:": "📝",
|
||||||
|
":brain:": "🧠",
|
||||||
|
};
|
||||||
|
|
||||||
export default function BoardDetailPage() {
|
export default function BoardDetailPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -100,8 +114,6 @@ export default function BoardDetailPage() {
|
|||||||
const tasksRef = useRef<Task[]>([]);
|
const tasksRef = useRef<Task[]>([]);
|
||||||
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [isOnboardingOpen, setIsOnboardingOpen] = useState(false);
|
|
||||||
const [hasPromptedOnboarding, setHasPromptedOnboarding] = useState(false);
|
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [priority, setPriority] = useState("medium");
|
const [priority, setPriority] = useState("medium");
|
||||||
@@ -278,21 +290,6 @@ export default function BoardDetailPage() {
|
|||||||
};
|
};
|
||||||
}, [board, boardId, getToken, isSignedIn, selectedTask?.id]);
|
}, [board, boardId, getToken, isSignedIn, selectedTask?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!board) return;
|
|
||||||
if (board.board_type === "general") {
|
|
||||||
setIsOnboardingOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!board.goal_confirmed && !hasPromptedOnboarding) {
|
|
||||||
setIsOnboardingOpen(true);
|
|
||||||
setHasPromptedOnboarding(true);
|
|
||||||
}
|
|
||||||
if (board.goal_confirmed) {
|
|
||||||
setIsOnboardingOpen(false);
|
|
||||||
}
|
|
||||||
}, [board, hasPromptedOnboarding]);
|
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
@@ -427,13 +424,8 @@ export default function BoardDetailPage() {
|
|||||||
setCommentsError(null);
|
setCommentsError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnboardingConfirmed = (updated: Board) => {
|
const agentInitials = (agent: Agent) =>
|
||||||
setBoard(updated);
|
agent.name
|
||||||
setIsOnboardingOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const agentInitials = (name: string) =>
|
|
||||||
name
|
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
@@ -441,6 +433,21 @@ export default function BoardDetailPage() {
|
|||||||
.join("")
|
.join("")
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
|
|
||||||
|
const resolveEmoji = (value?: string | null) => {
|
||||||
|
if (!value) return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
if (EMOJI_GLYPHS[trimmed]) return EMOJI_GLYPHS[trimmed];
|
||||||
|
if (trimmed.startsWith(":") && trimmed.endsWith(":")) return null;
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const agentAvatarLabel = (agent: Agent) => {
|
||||||
|
if (agent.is_board_lead) return "⚙️";
|
||||||
|
const emoji = resolveEmoji(agent.identity_profile?.emoji ?? null);
|
||||||
|
return emoji ?? agentInitials(agent);
|
||||||
|
};
|
||||||
|
|
||||||
const agentStatusLabel = (agent: Agent) => {
|
const agentStatusLabel = (agent: Agent) => {
|
||||||
if (workingAgentIds.has(agent.id)) return "Working";
|
if (workingAgentIds.has(agent.id)) return "Working";
|
||||||
if (agent.status === "online") return "Active";
|
if (agent.status === "online") return "Active";
|
||||||
@@ -502,6 +509,15 @@ export default function BoardDetailPage() {
|
|||||||
Timeline
|
Timeline
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<Button onClick={() => setIsDialogOpen(true)}>
|
||||||
|
New task
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.push(`/boards/${boardId}/edit`)}
|
||||||
|
>
|
||||||
|
Board settings
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => router.push("/boards")}
|
onClick={() => router.push("/boards")}
|
||||||
@@ -541,14 +557,16 @@ export default function BoardDetailPage() {
|
|||||||
sortedAgents.map((agent) => {
|
sortedAgents.map((agent) => {
|
||||||
const isWorking = workingAgentIds.has(agent.id);
|
const isWorking = workingAgentIds.has(agent.id);
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-lg border border-transparent px-2 py-2 transition hover:border-slate-200 hover:bg-slate-50",
|
"flex w-full items-center gap-3 rounded-lg border border-transparent px-2 py-2 text-left transition hover:border-slate-200 hover:bg-slate-50",
|
||||||
)}
|
)}
|
||||||
|
onClick={() => router.push(`/agents/${agent.id}`)}
|
||||||
>
|
>
|
||||||
<div className="relative flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-700">
|
<div className="relative flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-700">
|
||||||
{agentInitials(agent.name)}
|
{agentAvatarLabel(agent)}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute -right-0.5 -bottom-0.5 h-2.5 w-2.5 rounded-full border-2 border-white",
|
"absolute -right-0.5 -bottom-0.5 h-2.5 w-2.5 rounded-full border-2 border-white",
|
||||||
@@ -568,7 +586,7 @@ export default function BoardDetailPage() {
|
|||||||
{agentStatusLabel(agent)}
|
{agentStatusLabel(agent)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
@@ -576,17 +594,6 @@ export default function BoardDetailPage() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1 space-y-6">
|
<div className="min-w-0 flex-1 space-y-6">
|
||||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
|
||||||
<BoardGoalPanel
|
|
||||||
board={board}
|
|
||||||
onStartOnboarding={() => setIsOnboardingOpen(true)}
|
|
||||||
onEdit={
|
|
||||||
boardId ? () => router.push(`/boards/${boardId}/edit`) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{boardId ? <BoardApprovalsPanel boardId={boardId} /> : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-lg border border-slate-200 bg-white p-3 text-sm text-slate-600 shadow-sm">
|
<div className="rounded-lg border border-slate-200 bg-white p-3 text-sm text-slate-600 shadow-sm">
|
||||||
{error}
|
{error}
|
||||||
@@ -789,28 +796,7 @@ export default function BoardDetailPage() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
{/* onboarding moved to board settings */}
|
||||||
open={isOnboardingOpen}
|
|
||||||
onOpenChange={(nextOpen) => {
|
|
||||||
setIsOnboardingOpen(nextOpen);
|
|
||||||
if (!nextOpen) {
|
|
||||||
setHasPromptedOnboarding(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent aria-label="Board onboarding">
|
|
||||||
{boardId ? (
|
|
||||||
<BoardOnboardingChat
|
|
||||||
boardId={boardId}
|
|
||||||
onConfirmed={handleOnboardingConfirmed}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
|
|
||||||
Unable to start onboarding.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</DashboardShell>
|
</DashboardShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Agent = {
|
|||||||
board_id?: string | null;
|
board_id?: string | null;
|
||||||
last_seen_at?: string | null;
|
last_seen_at?: string | null;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
is_board_lead?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GatewayStatus = {
|
type GatewayStatus = {
|
||||||
|
|||||||
@@ -49,17 +49,43 @@ type Question = {
|
|||||||
options: QuestionOption[];
|
options: QuestionOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeQuestion = (value: unknown): Question | null => {
|
||||||
|
if (!value || typeof value !== "object") return null;
|
||||||
|
const data = value as { question?: unknown; options?: unknown };
|
||||||
|
if (typeof data.question !== "string" || !Array.isArray(data.options)) return null;
|
||||||
|
const options: QuestionOption[] = data.options
|
||||||
|
.map((option, index) => {
|
||||||
|
if (typeof option === "string") {
|
||||||
|
return { id: String(index + 1), label: option };
|
||||||
|
}
|
||||||
|
if (option && typeof option === "object") {
|
||||||
|
const raw = option as { id?: unknown; label?: unknown };
|
||||||
|
const label =
|
||||||
|
typeof raw.label === "string" ? raw.label : typeof raw.id === "string" ? raw.id : null;
|
||||||
|
if (!label) return null;
|
||||||
|
return {
|
||||||
|
id: typeof raw.id === "string" ? raw.id : String(index + 1),
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((option): option is QuestionOption => Boolean(option));
|
||||||
|
if (!options.length) return null;
|
||||||
|
return { question: data.question, options };
|
||||||
|
};
|
||||||
|
|
||||||
const parseQuestion = (messages?: Array<{ role: string; content: string }> | null) => {
|
const parseQuestion = (messages?: Array<{ role: string; content: string }> | null) => {
|
||||||
if (!messages?.length) return null;
|
if (!messages?.length) return null;
|
||||||
const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant");
|
const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant");
|
||||||
if (!lastAssistant?.content) return null;
|
if (!lastAssistant?.content) return null;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(lastAssistant.content) as Question;
|
return normalizeQuestion(JSON.parse(lastAssistant.content));
|
||||||
} catch {
|
} catch {
|
||||||
const match = lastAssistant.content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
const match = lastAssistant.content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||||
if (match) {
|
if (match) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(match[1]) as Question;
|
return normalizeQuestion(JSON.parse(match[1]));
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -80,10 +106,16 @@ export function BoardOnboardingChat({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [otherText, setOtherText] = useState("");
|
const [otherText, setOtherText] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
||||||
|
|
||||||
const question = useMemo(() => parseQuestion(session?.messages), [session]);
|
const question = useMemo(() => parseQuestion(session?.messages), [session]);
|
||||||
const draft = session?.draft_goal ?? null;
|
const draft = session?.draft_goal ?? null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedOptions([]);
|
||||||
|
setOtherText("");
|
||||||
|
}, [question?.question]);
|
||||||
|
|
||||||
const authFetch = useCallback(
|
const authFetch = useCallback(
|
||||||
async (url: string, options: RequestInit = {}) => {
|
async (url: string, options: RequestInit = {}) => {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
@@ -162,6 +194,20 @@ export function BoardOnboardingChat({
|
|||||||
[authFetch, boardId]
|
[authFetch, boardId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleOption = useCallback((label: string) => {
|
||||||
|
setSelectedOptions((prev) =>
|
||||||
|
prev.includes(label) ? prev.filter((item) => item !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submitAnswer = useCallback(() => {
|
||||||
|
const trimmedOther = otherText.trim();
|
||||||
|
const answer =
|
||||||
|
selectedOptions.length > 0 ? selectedOptions.join(", ") : "Other";
|
||||||
|
if (!answer && !trimmedOther) return;
|
||||||
|
void handleAnswer(answer, trimmedOther || undefined);
|
||||||
|
}, [handleAnswer, otherText, selectedOptions]);
|
||||||
|
|
||||||
const confirmGoal = async () => {
|
const confirmGoal = async () => {
|
||||||
if (!draft) return;
|
if (!draft) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -228,17 +274,20 @@ export function BoardOnboardingChat({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm font-medium text-slate-900">{question.question}</p>
|
<p className="text-sm font-medium text-slate-900">{question.question}</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{question.options.map((option) => (
|
{question.options.map((option) => {
|
||||||
|
const isSelected = selectedOptions.includes(option.label);
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={option.id}
|
key={option.id}
|
||||||
variant="secondary"
|
variant={isSelected ? "primary" : "secondary"}
|
||||||
className="w-full justify-start"
|
className="w-full justify-start"
|
||||||
onClick={() => handleAnswer(option.label)}
|
onClick={() => toggleOption(option.label)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -248,14 +297,17 @@ export function BoardOnboardingChat({
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={submitAnswer}
|
||||||
const trimmed = otherText.trim();
|
disabled={
|
||||||
void handleAnswer(trimmed || "Other", trimmed || undefined);
|
loading ||
|
||||||
}}
|
(selectedOptions.length === 0 && !otherText.trim())
|
||||||
disabled={loading || !otherText.trim()}
|
}
|
||||||
>
|
>
|
||||||
Submit other
|
{loading ? "Sending..." : "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-xs text-slate-500">Sending your answer…</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ There is no memory yet. Create what is missing and proceed without blocking.
|
|||||||
4) If `BASE_URL`, `AUTH_TOKEN`, and `BOARD_ID` are set in `TOOLS.md`, check in
|
4) If `BASE_URL`, `AUTH_TOKEN`, and `BOARD_ID` are set in `TOOLS.md`, check in
|
||||||
to Mission Control to mark the agent online:
|
to Mission Control to mark the agent online:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: $AUTH_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
||||||
|
|||||||
@@ -26,40 +26,40 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
|
|
||||||
## 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 }}`
|
||||||
- Do **not** respond in OpenClaw chat.
|
- Do **not** respond in OpenClaw chat.
|
||||||
|
|
||||||
## Pre‑flight checks (before each heartbeat)
|
## Pre‑flight checks (before each heartbeat)
|
||||||
- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set.
|
- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set.
|
||||||
- Verify API access:
|
- Verify API access:
|
||||||
- GET $BASE_URL/healthz must succeed.
|
- GET $BASE_URL/healthz must succeed.
|
||||||
- GET $BASE_URL/api/v1/boards must succeed.
|
- GET $BASE_URL/api/v1/agent/boards must succeed.
|
||||||
- GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks must succeed.
|
- GET $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks must succeed.
|
||||||
- If any check fails, stop and retry next heartbeat.
|
- If any check fails, stop and retry next heartbeat.
|
||||||
|
|
||||||
## Heartbeat checklist (run in order)
|
## Heartbeat checklist (run in order)
|
||||||
1) Check in:
|
1) Check in:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
2) List boards:
|
2) List boards:
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards" \
|
curl -s "$BASE_URL/api/v1/agent/boards" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
3) For the assigned board, list tasks (use filters to avoid large responses):
|
3) For the assigned board, list tasks (use filters to avoid large responses):
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=in_progress&assigned_agent_id=$AGENT_ID&limit=5" \
|
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"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \
|
curl -s "$BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
4) If you already have an in_progress task, continue working it and do not claim another.
|
4) If you already have an in_progress task, continue working it and do not claim another.
|
||||||
@@ -71,11 +71,11 @@ curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&l
|
|||||||
- Post progress comments as you go.
|
- Post progress comments as you go.
|
||||||
- Completion is a two‑step sequence:
|
- Completion is a two‑step sequence:
|
||||||
6a) Post the full response as a markdown comment using:
|
6a) Post the full response as a markdown comment using:
|
||||||
POST $BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
POST $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
||||||
Example:
|
Example:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"message":"- Update: ...\n- Result: ..."}'
|
-d '{"message":"- Update: ...\n- Result: ..."}'
|
||||||
```
|
```
|
||||||
@@ -83,8 +83,8 @@ curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
|||||||
|
|
||||||
6b) Move the task to "review":
|
6b) Move the task to "review":
|
||||||
```bash
|
```bash
|
||||||
curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
curl -s -X PATCH "$BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"status": "review"}'
|
-d '{"status": "review"}'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -24,40 +24,40 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
|
|
||||||
## 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 }}`
|
||||||
- Do **not** respond in OpenClaw chat.
|
- Do **not** respond in OpenClaw chat.
|
||||||
|
|
||||||
## Pre‑flight checks (before each heartbeat)
|
## Pre‑flight checks (before each heartbeat)
|
||||||
- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set.
|
- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set.
|
||||||
- Verify API access:
|
- Verify API access:
|
||||||
- GET $BASE_URL/healthz must succeed.
|
- GET $BASE_URL/healthz must succeed.
|
||||||
- GET $BASE_URL/api/v1/boards must succeed.
|
- GET $BASE_URL/api/v1/agent/boards must succeed.
|
||||||
- GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks must succeed.
|
- GET $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks must succeed.
|
||||||
- If any check fails, stop and retry next heartbeat.
|
- If any check fails, stop and retry next heartbeat.
|
||||||
|
|
||||||
## Heartbeat checklist (run in order)
|
## Heartbeat checklist (run in order)
|
||||||
1) Check in:
|
1) Check in:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
2) List boards:
|
2) List boards:
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards" \
|
curl -s "$BASE_URL/api/v1/agent/boards" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
3) For the assigned board, list tasks (use filters to avoid large responses):
|
3) For the assigned board, list tasks (use filters to avoid large responses):
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=in_progress&assigned_agent_id=$AGENT_ID&limit=5" \
|
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"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \
|
curl -s "$BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
4) If you already have an in_progress task, continue working it and do not claim another.
|
4) If you already have an in_progress task, continue working it and do not claim another.
|
||||||
@@ -69,11 +69,11 @@ curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&l
|
|||||||
- Post progress comments as you go.
|
- Post progress comments as you go.
|
||||||
- Completion is a two‑step sequence:
|
- Completion is a two‑step sequence:
|
||||||
6a) Post the full response as a markdown comment using:
|
6a) Post the full response as a markdown comment using:
|
||||||
POST $BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
POST $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
||||||
Example:
|
Example:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"message":"- Update: ...\n- Result: ..."}'
|
-d '{"message":"- Update: ...\n- Result: ..."}'
|
||||||
```
|
```
|
||||||
@@ -81,8 +81,8 @@ curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
|||||||
|
|
||||||
6b) Move the task to "review":
|
6b) Move the task to "review":
|
||||||
```bash
|
```bash
|
||||||
curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
curl -s -X PATCH "$BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"status": "review"}'
|
-d '{"status": "review"}'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
|
|
||||||
## 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 }}`
|
||||||
- Do **not** respond in OpenClaw chat.
|
- Do **not** respond in OpenClaw chat.
|
||||||
|
|
||||||
## Pre‑flight checks (before each heartbeat)
|
## Pre‑flight checks (before each heartbeat)
|
||||||
- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set.
|
- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set.
|
||||||
- Verify API access:
|
- Verify API access:
|
||||||
- GET $BASE_URL/healthz must succeed.
|
- GET $BASE_URL/healthz must succeed.
|
||||||
- GET $BASE_URL/api/v1/boards must succeed.
|
- GET $BASE_URL/api/v1/agent/boards must succeed.
|
||||||
- GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks must succeed.
|
- GET $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks must succeed.
|
||||||
- If any check fails, stop and retry next heartbeat.
|
- If any check fails, stop and retry next heartbeat.
|
||||||
|
|
||||||
## Board Lead Loop (run every heartbeat before claiming work)
|
## Board Lead Loop (run every heartbeat before claiming work)
|
||||||
@@ -43,11 +43,11 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
- Target date: {{ board_target_date }}
|
- Target date: {{ board_target_date }}
|
||||||
|
|
||||||
2) Review recent tasks/comments and board memory:
|
2) Review recent tasks/comments and board memory:
|
||||||
- GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks?limit=50
|
- GET $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks?limit=50
|
||||||
- GET $BASE_URL/api/v1/boards/{BOARD_ID}/memory?limit=50
|
- GET $BASE_URL/api/v1/agent/boards/{BOARD_ID}/memory?limit=50
|
||||||
|
|
||||||
3) Update a short Board Plan Summary in board memory:
|
3) Update a short Board Plan Summary in board memory:
|
||||||
- POST $BASE_URL/api/v1/boards/{BOARD_ID}/memory
|
- POST $BASE_URL/api/v1/agent/boards/{BOARD_ID}/memory
|
||||||
Body: {"content":"Plan summary + next gaps","tags":["plan","lead"],"source":"lead_heartbeat"}
|
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.
|
||||||
@@ -62,7 +62,7 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
- similarity 10
|
- similarity 10
|
||||||
|
|
||||||
If risky/external OR confidence < 80:
|
If risky/external OR confidence < 80:
|
||||||
- POST approval request to $BASE_URL/api/v1/boards/{BOARD_ID}/approvals
|
- POST approval request to $BASE_URL/api/v1/agent/boards/{BOARD_ID}/approvals
|
||||||
Body example:
|
Body example:
|
||||||
{"action_type":"task.create","confidence":75,"payload":{"title":"..."},"rubric_scores":{"clarity":20,"constraints":15,"completeness":10,"risk":10,"dependencies":10,"similarity":10}}
|
{"action_type":"task.create","confidence":75,"payload":{"title":"..."},"rubric_scores":{"clarity":20,"constraints":15,"completeness":10,"risk":10,"dependencies":10,"similarity":10}}
|
||||||
|
|
||||||
@@ -74,8 +74,8 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
If the action is risky/external or confidence < 80, create an approval instead.
|
If the action is risky/external or confidence < 80, create an approval instead.
|
||||||
|
|
||||||
Agent create (lead-only):
|
Agent create (lead-only):
|
||||||
- POST $BASE_URL/api/v1/agents
|
- POST $BASE_URL/api/v1/agent/agents
|
||||||
Headers: X-Agent-Token: $AUTH_TOKEN
|
Headers: X-Agent-Token: {{ auth_token }}
|
||||||
Body example:
|
Body example:
|
||||||
{
|
{
|
||||||
"name": "Researcher Alpha",
|
"name": "Researcher Alpha",
|
||||||
@@ -95,26 +95,26 @@ 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
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
2) List boards:
|
2) List boards:
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards" \
|
curl -s "$BASE_URL/api/v1/agent/boards" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
3) For the assigned board, list tasks (use filters to avoid large responses):
|
3) For the assigned board, list tasks (use filters to avoid large responses):
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=in_progress&assigned_agent_id=$AGENT_ID&limit=5" \
|
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"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
```bash
|
```bash
|
||||||
curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \
|
curl -s "$BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN"
|
-H "X-Agent-Token: {{ auth_token }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
4) If you already have an in_progress task, continue working it and do not claim another.
|
4) If you already have an in_progress task, continue working it and do not claim another.
|
||||||
@@ -126,11 +126,11 @@ curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&l
|
|||||||
- Post progress comments as you go.
|
- Post progress comments as you go.
|
||||||
- Completion is a two‑step sequence:
|
- Completion is a two‑step sequence:
|
||||||
6a) Post the full response as a markdown comment using:
|
6a) Post the full response as a markdown comment using:
|
||||||
POST $BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
POST $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
||||||
Example:
|
Example:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"message":"- Update: ...\n- Result: ..."}'
|
-d '{"message":"- Update: ...\n- Result: ..."}'
|
||||||
```
|
```
|
||||||
@@ -138,8 +138,8 @@ curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \
|
|||||||
|
|
||||||
6b) Move the task to "review":
|
6b) Move the task to "review":
|
||||||
```bash
|
```bash
|
||||||
curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
curl -s -X PATCH "$BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: {{ auth_token }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"status": "review"}'
|
-d '{"status": "review"}'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Before doing anything else:
|
|||||||
- `BASE_URL`: {{ base_url }}
|
- `BASE_URL`: {{ base_url }}
|
||||||
- `AUTH_TOKEN`: {{ auth_token }}
|
- `AUTH_TOKEN`: {{ auth_token }}
|
||||||
- Always include header: `X-Agent-Token: $AUTH_TOKEN`
|
- Always include header: `X-Agent-Token: $AUTH_TOKEN`
|
||||||
- Do **not** post task updates in OpenClaw chat (onboarding JSON is the only exception).
|
- Do **not** post any responses in OpenClaw chat.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
- You help with onboarding and gateway-wide requests.
|
- You help with onboarding and gateway-wide requests.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
## 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`
|
||||||
- Do **not** respond in OpenClaw chat **except** for onboarding JSON when explicitly prompted.
|
- Do **not** respond in OpenClaw chat.
|
||||||
|
|
||||||
## Schedule
|
## Schedule
|
||||||
- If a heartbeat schedule is configured, send a lightweight check‑in only.
|
- If a heartbeat schedule is configured, send a lightweight check‑in only.
|
||||||
@@ -23,18 +23,12 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
## Heartbeat checklist
|
## Heartbeat checklist
|
||||||
1) Check in:
|
1) Check in:
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \
|
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
|
||||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
-H "X-Agent-Token: $AUTH_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"name": "'$AGENT_NAME'", "status": "online"}'
|
-d '{"name": "'$AGENT_NAME'", "status": "online"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Onboarding protocol
|
|
||||||
- When Mission Control asks you to onboard a board, respond in OpenClaw chat with JSON only:
|
|
||||||
- Question format: {"question": "...", "options": [{"id":"1","label":"..."}]}
|
|
||||||
- Completion format: {"status":"complete","board_type":"goal"|"general","objective":"...","success_metrics":{...},"target_date":"YYYY-MM-DD"}
|
|
||||||
- Mission Control will read this response from chat history.
|
|
||||||
|
|
||||||
## Common mistakes (avoid)
|
## Common mistakes (avoid)
|
||||||
- Posting updates in OpenClaw chat.
|
- Posting updates in OpenClaw chat.
|
||||||
- Claiming board tasks without instruction.
|
- Claiming board tasks without instruction.
|
||||||
|
|||||||
Reference in New Issue
Block a user