robot-council / core
The core of Robot Council, a coordination service for fleets of AI coding agents
Requires
- php: ^8.4
- composer-runtime-api: ^2.2
- illuminate/contracts: ^13.23.0
- laravel/mcp: ^1.0
- laravel/sanctum: ^4.3.1
- laravel/socialite: ^5.31.0
- league/commonmark: ^2.8.1
- livewire/livewire: ^4.2.0
- spatie/laravel-package-tools: ^1.93.2
Requires (Dev)
- driftingly/rector-laravel: ^2.6.2
- larastan/larastan: ^3.13.0
- laravel/pint: ^1.32.1
- orchestra/testbench: ^11.2.0
- pestphp/pest: ^5.2.1
- pestphp/pest-plugin-browser: ^5.0.1
- pestphp/pest-plugin-phpstan: ^5.2.1
- pestphp/pest-plugin-rector: ^5.0.4
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan-deprecation-rules: ^2.0.5
- rector/rector: ^2.6.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.8.3
- v0.8.2
- v0.8.1
- v0.8.0
- v0.7.8
- v0.7.7
- v0.7.6
- v0.7.5
- v0.7.4
- v0.7.3
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.17
- v0.6.16
- v0.6.15
- v0.6.14
- v0.6.13
- v0.6.12
- v0.6.11
- v0.6.10
- v0.6.9
- v0.6.8
- v0.6.7
- v0.6.6
- v0.6.5
- v0.6.4
- v0.6.3
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.0
- dev-require-rest-for-github
- dev-interface-design-rule
- dev-readme-overview
- dev-wiki-source-of-truth-repo
- dev-fix-security-audit-names
- dev-fix-release-notes-copies-claim
- dev-claude-md-local-db-note
- dev-check-stylesheet-cross-platform
- dev-group-seats-by-machine
- dev-rename-own-installation
- dev-fix-feed-sequence-assumption
- dev-retire-a-seat
- dev-fix-browser-axe-injection
- dev-fix-github-refusal-page
- dev-changelog-v0.8.3
- dev-578-gate-claims
- dev-changelog-v0.8.2
- dev-574-debounce-stale
- dev-changelog-v0.8.1
- dev-569-blocker-state
- dev-changelog-v0.8.0
- dev-import-blocker-edges
- dev-seat-row-controls
- dev-nullable-declared-capacity
- dev-phpstan-2-3
- dev-changelog-v0.7.8
- dev-align-lane-table-columns
- dev-changelog-v0.7.7
- dev-lone-link-title-target
- dev-serialize-enrollment-and-rename
- dev-render-placement-and-lane-titles
- dev-warning-button-boundary
- dev-change-work-location
- dev-visible-dividers
- dev-rename-machine-label
- dev-port-upstream-workaround-skill
- dev-render-agent-markdown
- dev-create-browser-traces-dir
- dev-keep-new-tab-inline
- dev-stack-session-rows
- dev-session-row-separation
- dev-shortlist-qualifiers
- dev-owed-cards
- dev-owed-list
- dev-administration-group-by-developer
- dev-livewire-on-every-page
- dev-lanes-group-by-role-and-developer
- dev-changelog-v0.7.6
- dev-avatar-picture-or-letter
- dev-change-feed-table
- dev-changelog-v0.7.5
- dev-501-role-label-in-feed
- dev-390-owed-card-structure
- dev-416-repository-images
- dev-410-developer-avatars
- dev-387-external-links-new-tab
- dev-452-enrollment-decision-messages
- dev-494-full-width
- dev-487-time-zones-and-format
- dev-484-resolve-github-logins
- dev-486-glossary-entries
- dev-483-time-zone-picker
- dev-482-button-boundaries
- dev-485-stack-form-labels
- dev-481-align-feed-type-column
- dev-480-touch-target-controls
- dev-backlog-search-qualifiers
- dev-changelog-v0.7.4
- dev-session-id-apart-from-installation
- dev-changelog-v0.7.3
- dev-who-cuts-a-release
- dev-migrate-once-transactions
- dev-scroll-anchor-script
- dev-lane-hold-nothing-startable
- dev-all-live-sessions
- dev-changelog-v0.7.2
- dev-ci-budgets-412
- dev-session-ids-admin
- dev-issue-411
- dev-same-second-replacement
- dev-core-version
- dev-ticket-from-title
- dev-allowlist-events
- dev-session-labels
- dev-allowlist-page
- dev-hide-finished-tasks
- dev-allowlist-table
- dev-axe-harness
- dev-lane-free-spare-capacity
- dev-vocabulary
- dev-changelog-v0.7.1
- dev-developer-settings-tool
- dev-keyboard
- dev-task-result-from-github
- dev-reflow
- dev-tag-after-main-ci
- dev-ephemeral-sessions
- dev-target-size
- dev-changelog-v0.7.0
- dev-session-capacity
- dev-changelog-v0.6.17
- dev-aaa-contrast
- dev-changelog-v0.6.16
- dev-release-wiki-step
- dev-changelog-v0.6.15
- dev-type-scale
- dev-changelog-v0.6.14
- dev-accessibility-rule
- dev-withdraw-role-request
- dev-changelog-v0.6.13
- dev-rename-core-slots
- dev-changelog-v0.6.12
- dev-installation-token-format
- dev-changelog-v0.6.11
- dev-filter-boundaries
- dev-changelog-v0.6.10
- dev-backlog-github-app
- dev-changelog-v0.6.9
- dev-split-presence
- dev-changelog-v0.6.8
- dev-page-titles
- dev-changelog-v0.6.7
- dev-mirror-restricted-env
- dev-changelog-v0.6.6
- dev-feed-limit-wording
- dev-changelog-v0.6.5
- dev-session-end-wording
- dev-changelog-v0.6.4
- dev-lost-lock-answer
- dev-changelog-v0.6.3
- dev-follower-cursor
- dev-changelog-v0.6.2
- dev-readme-wiki-link
- dev-changelog-v0.6.1
- dev-log-orphan-edge-removal
- dev-changelog-v0.6.0
- dev-placement-instruction
- dev-session-platform
- dev-ordered-blocker-edges
- dev-sessions-list
- dev-lane-conditions
- dev-placement-warnings
- dev-ticket-shortlist
- dev-quiet-lanes
- dev-watcher-heartbeat
- dev-owed-items
- dev-gate-runs
- dev-backlog-meters
- dev-lanes-page
- dev-placement-invariants
- dev-lane-holds
- dev-github-webhook
- dev-placement-precondition
- dev-report-branch-after-start
- dev-developer-seat-settings
- dev-task-names-its-issue
- dev-addressed-narration
- dev-changelog-v0.5.0
- dev-retire-session-project-id
- dev-label-fleet-facing-housekeeping
- dev-comment-on-ci-passed
- dev-run-generator-tests-in-ci
- dev-unname-repository-in-shared-docs
- dev-warn-on-unresolved-aliases
- dev-fix-generator-guards
- dev-changelog-v0.4.0
- dev-keep-merged-head-branches
- dev-read-shape-fixtures
- dev-port-worktrees-rule
This package is auto-updated.
Last update: 2026-11-05 23:53:16 UTC
README
The core package of Robot Council, a coordination service for fleets of AI coding agents. It is installed into a host Laravel application, which it gives GitHub sign-in restricted to an allowlist of GitHub accounts, agent enrollment through the device-code flow, agent-session presence, task claims, named locks, and the fleet's change feed.
The wiki holds the rest of the documentation: Home indexes every page.
Documentation
The wiki is the source of truth for everything beyond installing the package. This README is an overview, the installation steps, and the table below.
| Page | What it covers |
|---|---|
| Upgrading | what to do when moving from one release to the next |
| Enrolling an Agent Machine | the device-code flow, tokens and the endpoints a client calls |
| The MCP Server | the tools an agent sees and how a refusal comes back |
| The Dashboard | every page, its route and who may open it |
| Seats and Assignment Hours | seats, parking, assignment hours and days off |
| Roles | build, ci and coordinator, what each may do, and how a role is granted |
| The Task Lifecycle | every status and transition, and placing work on a session |
| Placing Work | who is here, the shortlist, lane conditions, quiet lanes and lane holds |
| The Lane Board | the open-issue meters, and setting up the GitHub App that fills them |
| The GitHub Webhook | setting up the webhook, importing what was already open |
| Waiting on a Developer | what a coordinator learns when a task waits on a person |
| Presence | active, stale and gone, the thresholds, and what a session loses |
| Locks and Fences | when to take a lock, and what the fence is for |
| The Event Feed | cursors, paging and pruning |
| Reach and Visibility | who can read which events and which parts of a task |
| Mirroring to Slack | posting each event to a channel for people to read |
| Retention | how long events, tasks, sessions and locks are kept, and robot-council:doctor |
Requirements
- PHP 8.4 or later
- Laravel 13.23 or later
- Guzzle 7, which
laravel/socialitecurrently caps laravel/sanctum4.3.1 or later, which agent credentials are issued through- A
userstable keyed by an integer, a UUID, or a ULID. The package stores that key as text, so'007'and'7'are different developers. The key is limited to 64 characters, which every one of those shapes is comfortably inside; a longer one is refused rather than truncated, because two developers whose keys shared a 64-character prefix would otherwise collapse into one. - A
userstable that accepts a row carrying onlynameandemail.robot-council:installrelaxes the two columns Laravel's own skeleton makesNOT NULL; anotherNOT NULLcolumn with no default fails the first sign-in.
Installation
The package is not published on Packagist yet. In a host application, require it from this repository, then:
php artisan robot-council:install # writes two migrations; commit what it writes
php artisan migrate
robot-council:install writes the migration that relaxes your users table, and copies Sanctum's
personal_access_tokens migration if you do not already have it. It exits non-zero while
sanctum.expiration is set: Sanctum measures that from a token's creation, so it would cut off a
renewed agent session token and the agent holding it, whatever the token's own expiry says. Leave it
null.
Configure a GitHub OAuth app in config/services.php (github), publish config/robot-council.php to set the route prefix, and list the GitHub user IDs allowed to sign in:
ROBOT_COUNCIL_DEVELOPERS=1234567,2345678 ROBOT_COUNCIL_ADMINS=1234567
The lists are read on every request, so removing an ID locks that developer and their agents out immediately. On an application that runs php artisan config:cache, re-run that command after changing either list, or the cached list stays live.
Each list also admits the entries an administrator adds to robot_council_allowlist_entries, through RobotCouncil\Support\AllowlistEntries (#406; the page for it is #407). The environment lists stay in effect permanently: an entry there cannot be removed through the store, which answers FromConfiguration instead, so an administrator named in ROBOT_COUNCIL_ADMINS is always one and the deployment's configuration is always the way back in. A table change takes effect on the next request, and is recorded in the change feed as allowlist.entry_added or allowlist.entry_removed (#408), naming the list, the GitHub ID, the login and the administrator. Like installation.revoked, every session reads them, and a refused or repeated change writes nothing. Until the migration has run, the table reads as empty and only the environment lists apply.
The Access page adds an account by its login or its numeric ID (#484). It looks the value up through the GitHub App described on the Lane Board page, shows which account that is, and stores the ID once an administrator confirms it. It also shows the login of an allowlisted account that has not signed in yet, remembered in robot_council_github_accounts and refreshed by robot-council:refresh-github-logins, which robot-council.schedule.github_logins runs every fifteen minutes and which asks again about a login it has at most once a day, and about an ID GitHub could not name at most once an hour. Neither decides access: the ID alone admits. Without the App, or while GitHub cannot be reached, the page shows the ID as before, and an ID can still be added, marked as unconfirmed. A profile needs no App permission beyond what the counts already use.
The package records which GitHub account a user is in its own robot_council_github_identities table, rather than a column on your users table, because that mapping decides who the lists admit.
Give your own sanctum guard a provider, if you use Sanctum for your own API:
// config/auth.php 'guards' => [ 'sanctum' => ['driver' => 'sanctum', 'provider' => 'users'], ],
Sanctum's default leaves that provider null, which accepts a token belonging to any model at all, so
an agent's token would otherwise authenticate on your own auth:sanctum routes.
If you run behind a load balancer, a CDN, or any reverse proxy, configure trusted proxies. The
verification page asks a developer to compare the address a code was requested from against their
own, and both come from $request->ip(). With ->trustProxies(at: '*') that value is the
X-Forwarded-For header, which whoever requested the code controls — so the page's one piece of
evidence can be made to corroborate an attacker, and the rate limits on the two unauthenticated
endpoints can be evaded by rotating the header. Name your proxies, or their addresses, rather than
trusting all of them.
Schedule sanctum:prune-expired. Expired session tokens are refused but not deleted, and a
process that dies without ending its session leaves its row behind. The package prunes its own
expired device codes hourly; the tokens table is Sanctum's and yours.
The package's machine routes run no middleware group by default. They are stateless and bring
their own throttling, and an application's api group often is not: statefulApi() promotes a
matching request into a session request and answers the unauthenticated device endpoints with 419.
Add what you need to robot-council.routes.api_middleware.
If your users table needs more than name and email, bind SuppliesUserAttributes. A
developer signing in with GitHub for the first time gets a user row, and the package writes those two
columns — robot-council:install relaxes nullability on users.password and users.email because
they are the two the framework's own skeleton makes NOT NULL. Any other NOT NULL column with no
default — tenant_id, organization_id, role_id, a first_name/last_name pair — is yours to
fill:
use RobotCouncil\Support\Contracts\SuppliesUserAttributes; use RobotCouncil\Support\NewDeveloper; final class TenantUserAttributes implements SuppliesUserAttributes { public function for(NewDeveloper $developer): array { return [ 'name' => $developer->login, 'email' => $developer->email, 'tenant_id' => Tenant::current()->id, ]; } } // In your own service provider $this->app->bind(SuppliesUserAttributes::class, TenantUserAttributes::class);
What you return is force-filled and written as given; the package adds nothing back on top, and
still constructs and saves the model itself. NewDeveloper carries the GitHub ID, login, email and
avatar, and it is a class rather than a parameter list so later additions do not break your
implementation. robot-council:install names the columns it cannot fill, so you find out then
rather than at somebody's first sign-in.
Changing email is allowed and is yours to own: sign-in maps an account to a user by GitHub ID, not
by address, so it still works — but the duplicate check the package already ran used the GitHub
address, and a collision on the one you write surfaces as an integrity error.
A deleted user can hold a developer's email, and only you can free it. Sign-in never claims an
existing account by address, so a developer whose GitHub email already belongs to a user is refused.
If that user is invisible to your model — soft-deleted, or behind a tenant scope — the refusal names
that as the cause and says an administrator has to restore, remove, or re-address the account.
Laravel's default error page does not print an exception's message, so publish
errors/409.blade.php and render $exception->getMessage() if you want the developer to read it
rather than finding it in your log.
Index lower(email) on a large users table. The address lookup is case-insensitive, because
collations differ by host, and lower(email) = ? cannot use a plain b-tree index on email.
Measured on PostgreSQL 17 with 200,000 users: a sequential scan touching 1,667 shared buffers,
against 4 for the same lookup on an indexed exact match. It runs once per sign-in. create index on users (lower(email)) is what this predicate uses; the package adds no index to your table.
Development
composer install composer test # Pest composer analyse # PHPStan (level max) vendor/bin/pint --test # code style composer test:refactor # Rector (dry run)
Changelog
See CHANGELOG.
License
The MIT License (MIT). See License File.