open-agent-center is a local-first control plane for orchestrating multiple VS Code Copilot agent sessions on one Windows machine.
The current implementation focuses on the first practical slice of the system:
- register a project
- provision isolated git worktrees for agent sessions
- create isolated agent sessions bound to worktree paths and branches
- create parallel development tasks
- assign tasks to agent sessions
- launch VS Code windows for agent sessions
- persist local controller state for later dashboard and review work
Running several Copilot-driven development threads in parallel quickly breaks down without isolation and visibility. The first version of open-agent-center treats each VS Code window as a local agent session and gives it:
- a unique session identity
- a dedicated worktree path
- a dedicated branch
- a tracked task assignment
- a persistent event trail
This keeps the problem grounded in supported automation boundaries. The controller manages windows, state, and review flow; Copilot interaction stays inside VS Code.
Compatibility note: the current implementation still uses worker in the internal domain model and /api/workers routes. In this repository, worker currently means a VS Code-backed agent session rather than the higher-level agent identity.
The repository now contains a minimal TypeScript controller service with:
- local HTTP API
- repository-backed state persistence
- default JSON-backed state persistence in
.data/state.json - optional SQLite-backed state persistence in
.data/state.sqlite - in-memory domain model for projects, agent sessions, tasks, runs, artifacts, and events
- same-origin validation dashboard served by the controller
- dashboard operator controls for task creation, assignment, agent-session provisioning, launch, heartbeat, and branch sync
- a runtime adapter boundary keyed by
runtimeKind - project-aware task and agent-session binding so assignments stay inside a single repository context
- a VS Code window launcher using the
codeCLI - an application layer for orchestration use cases
- a git worktree provisioning service for new agent sessions
- a git diff inspection service for agent-session worktrees
Current source layout:
src/index.ts: server bootstrapsrc/application/controllerService.ts: orchestration use casessrc/routes/api.ts: HTTP routingsrc/services/stateRepository.ts: storage abstraction used by the application layersrc/services/stateRepositoryFactory.ts: startup selection for JSON vs SQLite repositoriessrc/services/stateStore.ts: JSON-backed repository implementationsrc/services/sqliteStateRepository.ts: SQLite-backed repository implementation usingsql.jssrc/services/windowManager.ts: agent session window launchersrc/services/worktreeManager.ts: agent-session git worktree provisioningsrc/services/diffService.ts: agent-session diff inspectionsrc/adapters/: runtime-specific adapter implementationssrc/infra/runtimeAdapterRegistry.ts: runtime adapter registry used by the application layersrc/queries/workerQueries.ts: agent session board read modelsrc/domain/types.ts: shared domain types
Available endpoints:
GET /GET /dashboardGET /dashboard.cssGET /dashboard.jsGET /healthGET /api/stateGET /api/projectsGET /api/workersGET /api/workers/:workerId/diffGET /api/tasksGET /api/tasks/:taskIdPOST /api/projectsPOST /api/projects/:projectId/archivePOST /api/projects/:projectId/worktreesPOST /api/workersPOST /api/workers/:workerId/heartbeatPOST /api/workers/:workerId/syncPOST /api/workers/:workerId/cleanupPOST /api/tasksPOST /api/assignmentsPOST /api/tasks/:taskId/transitionsPOST /api/tasks/:taskId/reviewPOST /api/workers/:workerId/launch
GET /api/workers now includes an additive runtimeCapabilities field on each worker summary when capability metadata is available for that runtime. These capability flags are derived at read time from the runtime adapter layer and are not persisted in JSON or SQLite state.
Requirements:
- Node.js 20.11 or newer
- VS Code
codeCLI available inPATH
Install dependencies:
npm installRun the controller in development mode:
npm run devRun the controller with SQLite-backed persistence instead of JSON:
OPEN_AGENT_CENTER_STORAGE=sqlite npm run devOn first boot in SQLite mode, the controller will import the existing .data/state.json snapshot if a SQLite database does not already exist.
Open the validation dashboard in your browser:
http://127.0.0.1:4317/dashboard
The root path redirects to /dashboard for convenience.
The dashboard now supports the main operator loop directly in the browser: register projects, create tasks, provision agent sessions, assign queued work, launch supported runtimes, send heartbeat updates, and trigger branch sync back to the repository default branch. Tasks can optionally be bound to a project, and worktree-backed sessions are automatically bound to their source project so cross-project assignment mistakes are rejected.
The intended browser-first onboarding flow is: register the repository in the dashboard, provision a worktree-backed agent session for that project, then create and assign tasks without dropping to curl.
The dashboard also includes a shared project scope control for summary cards, agent sessions, tasks, review queue, and recent events, so operators can isolate one repository without changing the action forms.
The worker board now surfaces additive runtime capability hints as part of each runtime cell. The provisioning form also shows the selected runtime's current capability summary before you create a new session. Worker actions are only disabled where the capability is explicit. For example, runtimes without controller-driven editor launch support keep their rows and history visible, but their Launch action is disabled until a concrete adapter implementation exists.
Agent sessions can now also be archived directly from the dashboard. The archive action currently removes the session worktree by default, marks the underlying worker record as archived, and blocks future assignment or heartbeat updates for that session.
Projects can now be archived through the API once they no longer have live agent sessions or open tasks. Archived projects remain in history, but reject new tasks, sessions, and worktree provisioning so repository ownership cannot silently reopen.
The dashboard project table now exposes the same archive action. Archived projects stay visible with archive metadata, while project selectors for task creation and worktree provisioning only show writable projects. If an archive attempt is blocked, the project row now shows the latest blocking agent sessions and tasks using readable names with ids as secondary detail, and those blockers can be clicked to jump to the matching session or task row.
Project identity note:
project.idis the real unique identifier in the current implementation.project.nameis not currently enforced as unique.repoPathis also not currently enforced as unique.
The current implementation exposes three primary lifecycle surfaces: projects, workers, and tasks.
Project lifecycle in the current implementation:
stateDiagram-v2
[*] --> Registered : POST /api/projects
state "Registered\narchivedAt = null" as Registered
state "Archived\narchivedAt != null" as Archived
state "Archive Blocked\noperation result only\nproject record unchanged" as ArchiveBlocked
Registered --> Archived : POST /api/projects/:projectId/archive\nno live workers\nand no open tasks
Registered --> ArchiveBlocked : POST /api/projects/:projectId/archive\nany worker.status != archived\nor any task.status not in {done, canceled}
ArchiveBlocked --> Registered : record ProjectArchiveBlocked event
Archived --> Archived : createTask/createWorker/createProjectWorktree\nrejected with PROJECT_ARCHIVED
Operator notes for this lifecycle:
Registeredis the normal writable state. The project can accept new tasks, workers, and worktree provisioning.ArchiveBlockedis not a persisted project status. It is an archive attempt result recorded as aProjectArchiveBlockedevent while the project itself stays registered.Archivedis terminal in v1. There is no unarchive or delete route, and new work is rejected withPROJECT_ARCHIVED.
Worker lifecycle in the current implementation:
stateDiagram-v2
[*] --> Idle : create worker
state "Idle\nno assigned task" as Idle
state "Active\nassigned task in progress" as Active
state "Blocked\nworker-reported blocked" as Blocked
state "Offline\nlaunch failed or stale heartbeat" as Offline
state "Archived\ncleanup/archive terminal state" as Archived
Idle --> Active : assign task
Active --> Idle : unassign | complete
Active --> Blocked : heartbeat status=blocked
Active --> Idle : move task to review
Blocked --> Active : heartbeat status=active
Blocked --> Idle : unassign | complete | cancel
Idle --> Offline : launch failure
Active --> Offline : stale heartbeat derived status
Blocked --> Offline : stale heartbeat derived status
Offline --> Idle : heartbeat status=idle
Offline --> Active : heartbeat status=active
Offline --> Blocked : heartbeat status=blocked
Idle --> Archived : worker cleanup/archive
Offline --> Archived : worker cleanup/archive
Active --> Archived : blocked\ncleanup rejected while assigned
Blocked --> Archived : blocked\ncleanup rejected while assigned
Operator notes for this lifecycle:
Idle,Active, andBlockedare explicit worker states written by assignment flow or heartbeat updates.Offlinecan be written directly on launch failure, and it can also be derived when a non-archived worker heartbeat becomes stale.- Moving a task to
reviewreleases the worker back toIdle; review belongs to the task lifecycle, not the worker lifecycle. Archivedis terminal in v1. Archived workers reject new assignment and heartbeat updates.
Task lifecycle in the current implementation:
stateDiagram-v2
[*] --> Queued : create task
state "Queued\nunassigned backlog" as Queued
state "In Progress\nassigned to worker" as InProgress
state "Review\nawaiting operator decision" as Review
state "Blocked\nwork cannot proceed" as Blocked
state "Done\nterminal success" as Done
state "Canceled\nterminal stop" as Canceled
Queued --> InProgress : assign task
InProgress --> Queued : unassign
InProgress --> Review : move to review
InProgress --> Blocked : mark blocked
InProgress --> Done : complete
InProgress --> Canceled : cancel
Blocked --> InProgress : assign task
Blocked --> Canceled : cancel
Review --> Review : approve\nreviewState = approved
Review --> Queued : request changes
Review --> Done : integrate success
Review --> Review : integrate blocked/conflicted
Review --> Done : complete
Review --> Canceled : cancel
Operator notes for this lifecycle:
Queued -> In Progress -> Review -> Doneis the primary happy path.approvedoes not move the task out ofReview; it records approval state and optional reviewer notes, then the task stays in review until integration or another decision happens.request changesreturns the task toQueued, which is why reassignment is a task transition rather than a separate review state.Blocked,Done, andCanceledare the exit branches from the main path.DoneandCanceledare terminal in v1.
The next operator slice is also available through the same dashboard task table: unassign active work, mark tasks blocked, move tasks into review, complete them, or cancel them without leaving the page.
Tasks in review also appear in a dedicated review queue panel. The dashboard inspects GET /api/tasks/:taskId and GET /api/workers/:workerId/diff to show the latest task detail, artifacts, and worker diff summary before you approve, request changes, or integrate the work.
The review panel also includes reviewer notes. When you submit approve, request changes, or integrate from the dashboard, any note you enter is stored as a note artifact for that task.
Run a type check:
npm run checkRun the focused automated tests:
npm testType-check the test files as well:
npm run check:testRun the unified lifecycle smoke suite for onboarding, cleanup, project archive, and review/integration flows:
npm run smoke:lifecycleRun a local smoke check for the browser-first onboarding flow: project registration, worker provisioning, project-bound task creation, and assignment:
npm run smoke:worktreeRun a local smoke check for successful project archive and archived-project write rejection:
npm run smoke:project-archiveRun a local smoke check for blocked project archive while the project still has active work:
npm run smoke:project-archive:blockedRun a local smoke check for worker archive and worktree cleanup:
npm run smoke:cleanupRun a lightweight smoke check for runtime capability exposure in GET /api/workers and the dashboard runtime capability UI wiring:
npm run smoke:dashboard-runtimeThis smoke will reuse an already-running controller on http://127.0.0.1:4317 when available. If no controller is reachable, it will start a temporary local controller, run the checks, and stop that process automatically.
Run a local smoke check for blocked worker cleanup while the worker is still assigned:
npm run smoke:cleanup:blockedRun a local smoke check for the review queue flow:
npm run smoke:reviewRun a local smoke check for the request-changes and reassignment flow:
npm run smoke:review:changesRun a local smoke check for the real integration conflict flow:
npm run smoke:review:conflictThe smoke script assumes the controller is already running on http://127.0.0.1:4317 and uses the current repository root as the registered project path. It now validates the same operator order as the dashboard: register the project, provision a worktree-backed worker, create a project-bound task, assign it, and confirm the worker/task bindings persisted. By default it leaves the generated branch and worktree in place for inspection. To clean up immediately, run:
pwsh -File scripts/smoke-provision-worktree.ps1 -Cleanupnpm run smoke:lifecycle is the umbrella entry point for the current operational lifecycle slices. It runs onboarding, worker cleanup success, worker cleanup blocked, project archive success, project archive blocked, review approve/integrate, review request-changes/reassign, and review conflict in sequence against the same controller.
npm run smoke:cleanup also assumes the controller is already running on http://127.0.0.1:4317. It provisions a worktree-backed worker, archives that worker through POST /api/workers/:workerId/cleanup, then confirms the worker is reported as archived and the worktree path no longer exists on disk.
npm run smoke:project-archive assumes the controller is already running on http://127.0.0.1:4317. It archives a clean project, confirms the project is persisted as archived, then verifies that task creation, worker creation, and worktree provisioning are all rejected with PROJECT_ARCHIVED.
npm run smoke:project-archive:blocked exercises the guard path: it creates a project-bound worker and task, assigns them, attempts project archive, expects HTTP 409 with PROJECT_ARCHIVE_BLOCKED, and confirms the latest ProjectArchiveBlocked event references the same blocker ids that the dashboard resolves back to readable worker and task names.
npm run smoke:cleanup:blocked exercises the guard path: it provisions a worker, assigns a project-bound task to that worker, attempts cleanup, expects HTTP 409 with WORKER_CLEANUP_BLOCKED, and confirms the worktree and assignment both remain intact.
The review smoke script also assumes the controller is already running on http://127.0.0.1:4317. By default it now validates a real assign -> review -> approve(notes) -> integrate flow against an isolated temporary local clone: it provisions a worktree-backed worker, creates a real commit on the worker branch, integrates it into the registered project's main, and confirms the resulting repository HEAD and file content changed as expected.
npm run smoke:review:changes runs the alternate scenario: assign -> review -> request_changes(notes) -> queued -> reassign -> review. This confirms the task is returned to the queue, released from its first worker, then successfully reassigned and resubmitted for review.
npm run smoke:review:conflict runs the real merge-conflict scenario: it creates conflicting commits on the worker branch and main, attempts integrate, then confirms the task stays in review, the integration result is conflicted, and the repository HEAD remains on the pre-existing main commit.
Example flow:
- Register your repository as a project.
- Create tasks.
- Ask the controller to provision one worker worktree per task.
- Assign tasks to workers.
- Launch the matching VS Code worker windows.
- Open
/dashboardto validate the resulting controller state visually.
Example project registration:
curl -X POST http://localhost:4317/api/projects \
-H "Content-Type: application/json" \
-d "{\"name\":\"open-agent-center\",\"repoPath\":\"C:/repo/open-agent-center\",\"defaultBranch\":\"main\"}"Example worker creation:
curl -X POST http://localhost:4317/api/workers \
-H "Content-Type: application/json" \
-d "{\"name\":\"worker-1\",\"projectId\":\"<project-id>\",\"worktreePath\":\"C:/repo/.worktrees/worker-1\",\"assignedBranch\":\"task/worker-1\"}"Example task creation:
curl -X POST http://localhost:4317/api/tasks \
-H "Content-Type: application/json" \
-d "{\"title\":\"Build worker board\",\"description\":\"Implement worker status grid\",\"priority\":\"high\",\"projectId\":\"<project-id>\"}"Example worktree-backed worker creation:
curl -X POST http://localhost:4317/api/projects/<project-id>/worktrees \
-H "Content-Type: application/json" \
-d "{\"workerName\":\"copilot-1\",\"branchBase\":\"worker-board\"}"Example project archive:
curl -X POST http://localhost:4317/api/projects/<project-id>/archiveYou can also attach the new worker directly to a task during provisioning:
curl -X POST http://localhost:4317/api/projects/<project-id>/worktrees \
-H "Content-Type: application/json" \
-d "{\"workerName\":\"copilot-1\",\"taskId\":\"<task-id>\"}"Example worker diff lookup:
curl http://localhost:4317/api/workers/<worker-id>/diffGET /api/workers now returns board-oriented live fields for each worker, including:
projectId: the repository context bound to the worker, if anyprojectName: the bound project name, when availablechangedFileCount: current modified or untracked file count from the worktreehasChanges: whether the worker currently has local changesheartbeatAgeMs: how old the latest worker heartbeat isisStale: whether the latest heartbeat is older than the configured timeoutlastSyncStatus: latest sync outcome derived from the event traillastSyncTargetBranch: the branch most recently synced againstlastSyncSummary: operator-friendly summary of the last sync result
GET /api/workers also supports dashboard-friendly query parameters:
status=idle|active|blocked|offlinehasChanges=true|falseisStale=true|falseincludeDiff=true|falsetaskId=<task id>branch=<branch name>lastSyncStatus=synced|conflictedsortBy=name|status|lastSeenAt|heartbeatAgeMs|changedFileCountsortOrder=asc|desclimit=<non-negative integer>offset=<non-negative integer>
GET /api/workers returns:
items: the current page of worker summariesincludesDiffMetrics: whether this response actually computed and included live diff fieldspagination.total: number of workers after filters are appliedpagination.limit: requested page sizepagination.offset: starting index into the filtered result setpagination.count: number of workers in the current pagepagination.hasMore: whether another page exists after the current one
includeDiff=false skips live diff collection unless the request still needs diff-derived behavior such as hasChanges=... filtering or sortBy=changedFileCount.
Example filtered worker board lookup:
curl "http://localhost:4317/api/workers?status=offline&hasChanges=true&sortBy=heartbeatAgeMs&sortOrder=desc"Example task- and sync-aware worker board lookup:
curl "http://localhost:4317/api/workers?taskId=830f184f-a195-4a21-9422-c45d3bb03317&lastSyncStatus=conflicted&branch=feature/controller-query-foundation"Example paginated worker board lookup:
curl "http://localhost:4317/api/workers?sortBy=name&sortOrder=asc&limit=2&offset=2"Example lightweight worker board lookup without diff sampling:
curl "http://localhost:4317/api/workers?includeDiff=false&sortBy=lastSeenAt&sortOrder=desc&limit=20"Example worker branch sync:
curl -X POST http://localhost:4317/api/workers/<worker-id>/sync \
-H "Content-Type: application/json" \
-d "{\"targetBranch\":\"main\"}"Example worker heartbeat:
curl -X POST http://127.0.0.1:4317/api/workers/<worker-id>/heartbeat \
-H "Content-Type: application/json" \
-d "{\"status\":\"active\"}"Heartbeat behavior:
- workers can report
idle,active, orblocked offlineis controller-derived, not worker-reported- a worker is treated as
offlinewhenlastSeenAtis older thanWORKER_HEARTBEAT_TIMEOUT_MS - the default timeout is 5 minutes
heartbeatAgeMsandisStaleare returned by worker and task read models so the UI does not need to reimplement timeout math
Example task lifecycle transition:
curl -X POST http://127.0.0.1:4317/api/tasks/<task-id>/transitions \
-H "Content-Type: application/json" \
-d "{\"action\":\"review\"}"Example review action:
curl -X POST http://127.0.0.1:4317/api/tasks/<task-id>/review \
-H "Content-Type: application/json" \
-d "{\"action\":\"approve\"}"The built-in dashboard is a same-origin operator surface for the current demo. It polls the existing controller APIs every few seconds and renders:
- controller health and refresh state
- summary cards for projects, workers, tasks, runs, artifacts, and events
- worker status insight cards for active, blocked, offline, and stale-heartbeat counts
- projects, workers, and tasks tables
- a recent event timeline
- lightweight action controls for task creation, worker provisioning, worker launch, and heartbeat updates
The workers table is backed by the worker board API, so it also shows controller-derived status, heartbeat freshness, and changed-file counts without you having to inspect raw JSON manually. The dashboard now also exposes small mutation controls so you can validate the main orchestration loop without switching to curl for every step.
Example task detail lookup:
curl http://localhost:4317/api/tasks/<task-id>Planned next implementation steps:
- review and integration queue
- SQLite-backed persistence and richer worker lifecycle history
This project intentionally does not yet automate Copilot chat internals. That is a higher-risk layer and comes after worktree isolation, task orchestration, and observability are stable.