A cell/main PM's i_will_plan could legally re-claim its own task from awaiting_pm_review (a CLAIM_RULES edge added for post-respawn recovery), resetting the task to in_progress and re-running submit_up -> pr_pass -> awaiting_pm_review forever: one Sentinel conventions child looped eleven full laps in four hours (14 reviews on one PR, 37 agent spawns) while its root's closure check fired eighteen times and the Main PM could never close anything. The claim edge is gone from every table that carried it — CLAIM_RULES, the claim ActionSpec's source statuses, the StatusTransition row, the service-layer _ROLE_CLAIM_STATUSES twin, and the legacy enforcement shim's operational-edge/role-gate entries (left divergent, it would be the same silent two-table drift that produced this bug). The respawn case the edge existed for is now served properly: _handle_pm_reentry gained a third contract — a PM calling i_will_plan on its own awaiting_pm_review task gets a steering envelope (no claim, no state change) pointing at complete/request_changes, and give_me_work's next hint for that status says the same instead of steering back into i_will_plan. The no-transition review-claim path and the pm-review dispatch prompt were already correct and are untouched, so closure still converges through them. A new bidirectional test asserts CLAIM_RULES and _ROLE_CLAIM_STATUSES stay identical per PM role (the old comment claimed a sync test existed; it checked one direction only). Lifecycle artifacts regenerated; the parity suite's three unshaped session mocks fixed, zero AsyncMock warnings remain.
RoboCo Control Panel
Next.js 16 control panel for the RoboCo AI agent system. Formerly a separate repository (rennf93/roboco-panel), now vendored under panel/ in this monorepo so docker compose up -d brings up the whole stack from one place.
Stack
- Next.js 16 (App Router, standalone output)
- TypeScript
- Tailwind CSS
- Radix UI primitives
dnd-kitfor drag/drop (kanban)- pnpm for package management
Running in production (the normal path)
Use the root-level Docker Compose:
# from the repo root (one level up from this directory)
docker compose up -d
The panel is built as part of the compose stack via docker/panel.Dockerfile and served internally on port 3000. Nginx (also in the compose stack) is the single externally-exposed service on http://localhost:3000 and routes:
/api/*and/ws/*→ orchestrator (FastAPI, port 8000)- everything else → the Next.js panel
The panel uses relative URLs (/api/v1, /ws) so nothing here needs a backend URL in .env.
Running the panel alone for UI development
cd panel
pnpm install
pnpm dev
That gives you Next dev-server on localhost:3000, but you still need the orchestrator reachable at localhost:8000 (or via nginx) for API calls to work. Easiest: docker compose up -d the backend services, then run pnpm dev against that.
Build scripts
pnpm dev— development server with hot reloadpnpm build— production build (outputs.next/standalone/)pnpm start— run the standalone buildpnpm lint— ESLint
Where things live
src/app/— Next.js App Router pagessrc/components/— React components (organized by feature: tasks, agents, channels, …)src/lib/api/— typed API client (thin wrappers overfetch)src/lib/— constants, utilities, WebSocket hookssrc/types/— shared TypeScript types mirroring backend schemas (includesNotificationTypeenum)src/hooks/— reusable React hooks (see Frontend hooks)src/app/(dashboard)/notifications/— notifications inbox page and components
Hooks
The panel exposes public hooks under @/hooks. See Frontend hooks for full API reference and examples.
usePageRefresh
Page-scoped refresh coordinator. Pages register data-refetch callbacks; the navbar refresh button in src/components/layout/header.tsx calls refresh() and reflects the combined loading/disabled state. The button is disabled when no callbacks are registered (the registry is empty) and while a refresh is in progress.
import { usePageRefresh } from "@/hooks";
const { register, unregister, refresh, loading, disabled } = usePageRefresh();
disabledistruewhen no callbacks are registered (there is nothing to refresh)disabledbecomesfalseonce a callback is registereddisabledreturns totruewhen all callbacks are unregistered
Wrap your page or layout in PageRefreshProvider from @/components/providers before consuming the hook. Dashboard pages should register their refetch callbacks and avoid adding inline "Refresh" buttons; see docs/frontend/components/page-refresh-provider.md for the full wiring list and examples.
Notifications
The panel renders five core coordination-event notification types that signal task lifecycle transitions between agents:
| Type | Icon | Color | Meaning |
|---|---|---|---|
TASK_ASSIGNMENT |
ListTodo | green | A task has been assigned to you |
BLOCKER_ESCALATION |
AlertTriangle | red | A developer is blocked and escalated |
REVIEW_REQUEST |
Check | purple | Your review is needed |
DOCUMENTATION_REQUEST |
Info | blue | Documentation is needed |
APPROVAL |
ShieldCheck | emerald | Board-level approval requested |
Each notification optionally carries a related_task_id rendered as a deep-link to /tasks/{id}. For full details on types, icons, and adding new types, see docs/frontend/components/notification-types.md.
Dependency Management
Version Alignment
When bumping Next.js, always update eslint-config-next to match the same version. These packages must stay in sync. See UPGRADE.md for detailed procedures and troubleshooting.
Backend schema changes
When the backend changes response shapes, mirror them in src/types/ and the relevant src/lib/api/ module. Keep API paths relative so nginx routing keeps working.