* Restore foundry onboarding stack * Consolidate foundry rename * Create foundry tasks without prompts * Rename Foundry handoffs to tasks
17 KiB
RivetKit + Sandbox Provider + OpenTUI Migration Spec
Status: implemented baseline (Phase 1-4 complete, Phase 5 partial) Date: 2026-02-08
Locked Decisions
- Entire rewrite is TypeScript. All Rust code will be deleted at cutover.
- Repo stays a single monorepo, managed with
pnpmworkspaces + Turborepo. corepackage is renamed toshared.integrationsandproviderslive inside the backend package (not top-level packages).- Rivet-backed state uses SQLite + Drizzle only.
- RivetKit dependencies come from local
../rivetbuilds only; no published npm packages. - Everything is workspace-scoped. Workspace is configurable from CLI.
ControlPlaneActoris renamed toWorkspaceActor(workspace coordinator).- Every actor key is prefixed by workspace.
--workspaceis optional; commands resolve workspace via flag -> config default ->default.- RivetKit local dependency wiring is
link:-based. - Keep the existing config file path (
~/.config/foundry/config.toml) and evolve keys in place. .agentsand skill files are in scope for migration updates.- Parent orchestration actors (
workspace,project,task) use command-only loops with no timeout. - Periodic syncing/polling runs in dedicated child actors, each with a single timeout cadence.
- For each actor, define the main loop and exactly what data it mutates; keep single-writer ownership strict.
Executive Summary
We will replace the existing Rust backend/CLI/TUI with TypeScript services and UIs:
- Backend: RivetKit actor runtime
- Agent orchestration: Sandbox Agent through provider adapters
- CLI: TypeScript
- TUI: TypeScript + OpenTUI
- State: SQLite + Drizzle (actor-owned writes)
The core architecture changes from "worktree-per-task" to "provider-selected sandbox-per-task." Local worktrees remain supported through a worktree provider.
Breaking Changes (Intentional)
- Rust binaries/backend removed.
- Existing IPC replaced by new TypeScript transport.
- Configuration schema changes for workspace selection and sandbox provider defaults.
- Runtime model changes from global control plane to workspace coordinator actor.
- Database schema migrates to workspace + provider + sandbox identity model.
- Command options evolve to include workspace and provider selection.
Monorepo and Build Tooling
Root tooling is standardized:
pnpm-workspace.yamlturbo.json- workspace scripts through
pnpm+turbo run ...
Target package layout:
packages/
shared/ # shared types, contracts, validation
backend/
src/
actors/
workspace.ts
project.ts
task.ts
sandbox-instance.ts
history.ts
project-pr-sync.ts
project-branch-sync.ts
task-status-sync.ts
keys.ts
events.ts
registry.ts
index.ts
providers/ # provider-api + implementations
provider-api/
worktree/
daytona/
integrations/ # sandbox-agent + git/github/graphite adapters
sandbox-agent/
git/
github/
graphite/
db/ # drizzle schema, queries, migrations
schema.ts
client.ts
migrations/
transport/
server.ts
types.ts
config/
workspace.ts
backend.ts
cli/ # hf command surface
src/
commands/
client/ # backend transport client
workspace/ # workspace selection resolver
tui/ # OpenTUI app
src/
app/
views/
state/
client/ # backend stream client
research/specs/
rivetkit-opentui-migration-plan.md (this file)
CLI and TUI are separate packages in the same monorepo, not separate repositories.
Actor File Map (Concrete)
Backend actor files and responsibilities:
packages/backend/src/actors/workspace.ts
WorkspaceActorimplementation.- Provider profile resolution and workspace-level coordination.
- Spawns/routes to
ProjectActorhandles.
packages/backend/src/actors/project.ts
ProjectActorimplementation.- Branch snapshot refresh, PR cache orchestration, stream publication.
- Routes task actions to
TaskActor.
packages/backend/src/actors/task.ts
TaskActorimplementation.- Task lifecycle, session/sandbox orchestration, post-idle automation.
packages/backend/src/actors/sandbox-instance.ts
SandboxInstanceActorimplementation.- Provider sandbox lifecycle, heartbeat, reconnect/recovery.
packages/backend/src/actors/history.ts
HistoryActorimplementation.- Writes workflow events to SQLite via Drizzle.
packages/backend/src/actors/keys.ts
- Workspace-prefixed actor key builders/parsers.
packages/backend/src/actors/events.ts
- Internal actor event envelopes and stream payload types.
packages/backend/src/actors/registry.ts
- RivetKit registry setup and actor registration.
packages/backend/src/actors/index.ts
- Actor exports and composition wiring.
packages/backend/src/actors/project-pr-sync.ts
- Read-only PR polling loop (single timeout cadence).
- Sends sync results back to
ProjectActor.
packages/backend/src/actors/project-branch-sync.ts
- Read-only branch snapshot polling loop (single timeout cadence).
- Sends sync results back to
ProjectActor.
packages/backend/src/actors/task-status-sync.ts
- Read-only session/sandbox status polling loop (single timeout cadence).
- Sends status updates back to
TaskActor.
RivetKit Source Policy (Local Only)
Do not use published RivetKit packages.
- Build RivetKit from local source:
cd ../rivet
pnpm build -F rivetkit
- Consume via local
link:dependencies to built artifacts. - Keep dependency wiring deterministic and documented in repo scripts.
Workspace Model
Every command executes against a resolved workspace context.
Workspace selection:
- CLI flag:
--workspace <name> - Config default workspace
- Fallback to
default
Workspace controls:
- provider profile defaults
- sandbox policy
- repo membership / resolution
- actor namespaces and database partitioning
New Actor Implementation Overview
RivetKit registry actor keys are workspace-prefixed:
WorkspaceActor(workspace coordinator)
- Key:
["ws", workspaceId] - Owns workspace config/runtime coordination, provider registry, workspace health.
- Resolves provider defaults and workspace-level policies.
ProjectActor
- Key:
["ws", workspaceId, "project", repoId] - Owns repo snapshot cache and PR cache refresh orchestration.
- Routes branch/task commands to task actors.
- Streams project updates to CLI/TUI subscribers.
TaskActor
- Key:
["ws", workspaceId, "project", repoId, "task", taskId] - Owns task metadata/runtime state.
- Creates/resumes sandbox + session through provider adapter.
- Handles attach/push/sync/merge/archive/kill and post-idle automation.
SandboxInstanceActor(optional but recommended)
- Key:
["ws", workspaceId, "provider", providerId, "sandbox", sandboxId] - Owns sandbox lifecycle, heartbeat, endpoint readiness, recovery.
HistoryActor
- Key:
["ws", workspaceId, "project", repoId, "history"] - Owns
eventswrites and workflow timeline completeness.
ProjectPrSyncActor(child poller)
- Key:
["ws", workspaceId, "project", repoId, "pr-sync"] - Polls PR state on interval and emits results to
ProjectActor. - Does not write DB directly.
ProjectBranchSyncActor(child poller)
- Key:
["ws", workspaceId, "project", repoId, "branch-sync"] - Polls branch/worktree state on interval and emits results to
ProjectActor. - Does not write DB directly.
TaskStatusSyncActor(child poller)
- Key:
["ws", workspaceId, "project", repoId, "task", taskId, "status-sync"] - Polls agent/session/sandbox health on interval and emits results to
TaskActor. - Does not write DB directly.
Ownership rule: each table/row has one actor writer.
Single-Writer Mutation Map
Always define actor run-loop + mutated state together:
WorkspaceActor
- Mutates:
workspaces,workspace_provider_profiles.
ProjectActor
- Mutates:
repos,branches,pr_cache(applies child poller results).
TaskActor
- Mutates:
tasks,task_runtime(applies child poller results).
SandboxInstanceActor
- Mutates:
sandbox_instances.
HistoryActor
- Mutates:
events.
- Child sync actors (
project-pr-sync,project-branch-sync,task-status-sync)
- Mutates: none (read-only pollers; publish result messages only).
Run Loop Patterns (Required)
Parent orchestration actors: no timeout, command-only queue loops.
WorkspaceActor (no timeout)
run: async (c) => {
while (true) {
const msg = await c.queue.next("workspace.command");
await handleWorkspaceCommand(c, msg); // writes workspace-owned tables only
}
};
ProjectActor (no timeout)
run: async (c) => {
while (true) {
const msg = await c.queue.next("project.command");
await handleProjectCommand(c, msg); // includes applying sync results to branches/pr_cache
}
};
TaskActor (no timeout)
run: async (c) => {
while (true) {
const msg = await c.queue.next("task.command");
await handleTaskCommand(c, msg); // includes applying status results to task_runtime
}
};
SandboxInstanceActor (no timeout)
run: async (c) => {
while (true) {
const msg = await c.queue.next("sandbox_instance.command");
await handleSandboxInstanceCommand(c, msg); // sandbox_instances table only
}
};
HistoryActor (no timeout)
run: async (c) => {
while (true) {
const msg = await c.queue.next("history.command");
await persistEvent(c, msg); // events table only
}
};
Child sync actors: one timeout each, one cadence each.
ProjectPrSyncActor (single timeout cadence)
run: async (c) => {
const intervalMs = 30_000;
while (true) {
const msg = await c.queue.next("project.pr_sync.command", { timeout: intervalMs });
if (!msg) {
const result = await pollPrState();
await sendToProject({ name: "project.pr_sync.result", result });
continue;
}
await handlePrSyncControl(c, msg); // force/stop/update-interval
}
};
ProjectBranchSyncActor (single timeout cadence)
run: async (c) => {
const intervalMs = 5_000;
while (true) {
const msg = await c.queue.next("project.branch_sync.command", { timeout: intervalMs });
if (!msg) {
const result = await pollBranchState();
await sendToProject({ name: "project.branch_sync.result", result });
continue;
}
await handleBranchSyncControl(c, msg);
}
};
TaskStatusSyncActor (single timeout cadence)
run: async (c) => {
const intervalMs = 2_000;
while (true) {
const msg = await c.queue.next("task.status_sync.command", { timeout: intervalMs });
if (!msg) {
const result = await pollSessionAndSandboxStatus();
await sendToTask({ name: "task.status_sync.result", result });
continue;
}
await handleStatusSyncControl(c, msg);
}
};
Sandbox Provider Interface
Provider contract lives under packages/backend/src/providers/provider-api and is consumed by workspace/project/task actors.
interface SandboxProvider {
id(): string;
capabilities(): ProviderCapabilities;
validateConfig(input: unknown): Promise<ValidatedConfig>;
createSandbox(req: CreateSandboxRequest): Promise<SandboxHandle>;
resumeSandbox(req: ResumeSandboxRequest): Promise<SandboxHandle>;
destroySandbox(req: DestroySandboxRequest): Promise<void>;
ensureSandboxAgent(req: EnsureAgentRequest): Promise<AgentEndpoint>;
health(req: SandboxHealthRequest): Promise<SandboxHealth>;
attachTarget(req: AttachTargetRequest): Promise<AttachTarget>;
}
Initial providers:
worktree
- Local git worktree-backed sandbox.
- Sandbox Agent local/shared endpoint.
- Preserves tmux +
cdergonomics.
daytona
- Remote sandbox lifecycle via Daytona.
- Boots/ensures Sandbox Agent inside sandbox.
- Returns endpoint/token for session operations.
Command Surface (Workspace + Provider Aware)
hf create ... --workspace <ws> --provider <worktree|daytona>hf switch --workspace <ws> [target]hf attach --workspace <ws> [task]hf list --workspace <ws>hf kill|archive|merge|push|sync --workspace <ws> ...hf workspace use <ws>to set default workspace
List/TUI include provider and sandbox health metadata.
--workspace remains optional; omitted values use the standard resolution order.
Data Model v2 (SQLite + Drizzle)
All persistent state is SQLite via Drizzle schema + migrations.
Tables (workspace-scoped):
workspacesworkspace_provider_profilesrepos(workspace_id,repo_id, ...)branches(workspace_id,repo_id, ...)tasks(workspace_id,task_id,provider_id, ...)task_runtime(workspace_id,task_id,sandbox_id,session_id, ...)sandbox_instances(workspace_id,provider_id,sandbox_id, ...)pr_cache(workspace_id,repo_id, ...)events(workspace_id,repo_id, ...)
Migration approach: one-way migration from existing schema during TS backend bootstrap.
Transport and Runtime
- TypeScript backend exposes local control API (socket or localhost HTTP).
- CLI/TUI are thin clients; all mutations go through backend actors.
- OpenTUI subscribes to project streams from workspace-scoped project actors.
- Workspace is required context on all backend mutation requests.
CLI/TUI are responsible for resolving workspace context before calling backend mutations.
CLI + TUI Packaging
CLI and TUI ship from one package:
packages/cli
- command-oriented UX (
hf create,hf push, scripting, JSON output) - interactive OpenTUI mode via
hf tui - shared client/runtime wiring in one distributable
The package still calls the same backend API and shares contracts from packages/shared.
Implementation Phases
Phase 0: Contracts and Workspace Spec
- Freeze workspace model, provider contract, and actor ownership map.
- Freeze command flags for workspace + provider selection.
- Define Drizzle schema draft and migration plan.
Exit criteria:
- Approved architecture RFC.
Phase 1: TypeScript Monorepo Bootstrap
- Add
pnpmworkspace + Turborepo pipeline. - Create
shared,backend, andclipackages (with TUI integrated into CLI). - Add strict TypeScript config and CI checks.
Exit criteria:
pnpm -w typecheckandturbo run buildpass.
Phase 2: RivetKit + Drizzle Foundations
- Wire local RivetKit dependency from
../rivet. - Add SQLite + Drizzle migrations and query layer.
- Implement actor registry with workspace-prefixed keys.
Exit criteria:
- Backend boot + workspace actor health checks pass.
Phase 3: Provider Layer in Backend
- Implement provider API inside backend package.
- Implement
worktreeprovider end-to-end. - Integrate sandbox-agent session lifecycle through provider.
Exit criteria:
create/list/switch/attach/push/sync/killpass on worktree provider.
Phase 4: Workspace/Task Lifecycle
- Implement workspace coordinator flows.
- Implement TaskActor full lifecycle + post-idle automation.
- Implement history events and PR/CI/review change tracking.
Exit criteria:
- history/event completeness with parity checks.
Phase 5: Daytona Provider
- Implement Daytona sandbox lifecycle adapter.
- Ensure sandbox-agent boot and reconnection behavior.
- Validate attach/switch/kill flows for remote sandboxes.
Exit criteria:
- e2e pass on daytona provider.
Phase 6: OpenTUI Rewrite
- Build interactive list/switch UI in OpenTUI.
- Implement key actions (attach/open PR/archive/merge/sync).
- Add workspace switcher UX and provider/sandbox indicators.
Exit criteria:
- TUI parity and responsive streaming updates.
Phase 7: Cutover + Rust Deletion
- Migrate existing DB to v2.
- Replace runtime entrypoints with TS CLI/backend/TUI.
- Delete Rust code, Cargo files, Rust scripts.
- Update docs and
skills/SKILL.md.
Exit criteria:
- no Rust code remains, fresh install + upgrade validated.
Testing Strategy
- Unit tests
- actor actions and ownership rules
- provider adapters
- event emission correctness
- drizzle query/migration tests
- Integration tests
- backend + sqlite + provider fakes
- workspace isolation boundaries
- session recovery and restart handling
- E2E tests
- worktree provider in local test repo
- daytona provider in controlled env
- OpenTUI interactive flows
- Reliability tests
- sandbox-agent restarts
- transient provider failures
- backend restart with in-flight tasks
Open Questions To Resolve Before Implementation
- Daytona production adapter parity:
- Current
daytonaprovider in this repo is intentionally a fallback adapter so local development remains testable without a Daytona backend. - Final deployment integration should replace placeholder lifecycle calls with Daytona API operations (create/destroy/health/auth/session boot inside sandbox).