- agent-job image: add neovim, tmux, less, unzip, wget, locales for the interactive shell (tmux powers cross-reconnect session persistence) - Wire NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL as a build arg (Dockerfile + compose.yml) since NEXT_PUBLIC vars are inlined at build time - docs/agent-terminal.md: architecture, env, nginx WS exposure, dev testing, security; note the build-time var in docs/compose.prod.yml
Spoon
Fork freely & keep them all intimately close to upstream.
Spoon is a self-hostable fork maintenance cockpit built around managed forks, durable maintenance threads, and OpenCode-powered workspaces.
What this is · Product model · Architecture · Environment
What This Is
Spoon is a private, actively evolving project for making forks less lonely to maintain.
Forking a project is easy. Keeping that fork close to upstream after you add custom changes is the hard part. Spoon treats a fork as an ongoing relationship: it watches upstream, understands fork-only commits, automatically syncs clean drift when it can, and opens a durable Thread when a decision needs context or code.
The application is currently GitHub-first. Future provider-neutral fields exist in the data model, but GitHub is the active automation surface today.
Highlights
-
Managed forks, called Spoons
Track upstream metadata, fork metadata, clone URLs, extra remotes, sync cadence, production-ref strategy, fork-only commits, and pull requests. -
Thread-first maintenance
Upstream updates, conflict review, ignore decisions, user-requested work, worker output, and draft PR handoff all live inside Threads. -
Clean drift auto-sync
If upstream moves and the fork has no custom commits, Spoon can fast-forward the fork without creating busywork. -
Custom forks get context
If the fork has custom commits, Spoon creates a maintenance thread rather than pretending the update is trivial. -
Effective drift
Spoon keeps raw GitHub drift visible while also tracking ignored upstream changes so irrelevant commits do not keep a fork permanently actionable. -
OpenCode workspaces
Agent work happens in an isolated workspace with a file tree, browser editor, diff viewer, command panel, logs, artifacts, and draft PR actions. -
User-owned providers and secrets
AI provider profiles, Codex/OpenCode auth, and per-Spoon project secrets are encrypted. Secrets are redacted from logs and refused from commits when materialized into env files. -
Draft PR handoff
Code changes become branches and draft pull requests. Spoon does not auto-merge custom forks behind the user's back.
Product Model
Spoons
A Spoon is a managed fork. It records the upstream project, the fork repository, default branches, sync policy, extra remotes, current drift, cached commits, cached pull requests, secrets, and agent settings.
Spoons are the durable project-level objects. They answer:
- What did I fork?
- Where does my fork live?
- How far has it drifted?
- Which commits are mine?
- Which upstream changes matter?
- What threads or PRs are open?
Threads
A Thread is the durable place where Spoon talks about maintenance work. Threads can be created by a user or by the system.
Common thread sources:
user_request: user asks Spoon to change a fork.upstream_update: upstream moved and the fork needs review.merge_conflict: a sync conflict needs context or code.manual_review: user explicitly asks for a review.system: internal maintenance coordination.
Threads hold messages, status, outcomes, related sync runs, related jobs, workspace links, draft PR links, and ignored upstream decisions.
Opening a thread opens its workspace when a run exists. The workspace is the
primary surface for that thread: agent messages, tool activity, file edits,
manual edits, diffs, commands, and draft PR actions all happen there. Legacy
job URLs under /spoons/[spoonId]/agent/[jobId] are kept for compatibility,
but normal navigation targets /threads/[threadId].
Maintenance decisions
Spoon's maintenance policy is intentionally conservative:
| Situation | Default action |
|---|---|
| No fork-only commits and upstream is ahead | Auto-sync |
| Fork-only commits and upstream is ahead | Create a maintenance thread |
| Merge conflicts | Open or continue a workspace thread |
| Irrelevant upstream changes | Record an intentional ignore decision |
| Agent/code changes | Open a draft PR |
The goal is to keep forks close without hiding risk or skipping review when custom work exists.
OpenCode workspaces
Spoon's optional agent worker is designed to run outside Convex actions. The worker claims queued jobs, clones the current GitHub fork, creates a branch, starts an isolated workspace, and exposes workspace operations to the Next app through server-only API proxies.
Workspace capabilities:
- browse repository files
- edit files in a browser editor
- use optional Vim keybindings
- resize the agent thread panel on desktop
- inspect diffs
- send thread messages to the agent
- run configured commands
- store logs and artifacts
- push a branch
- open a draft PR
The browser never receives worker tokens and never talks directly to the worker or job container.
Worker cleanup is available in Settings -> Worker. It can delete old terminal
workspace records and ask the active worker to remove orphaned job containers
and inactive work directories.
Local worker development:
scripts/build-agent-images
bun smoke:agent-container
bun dev:next:worker
bun dev:next:worker:staging
Local host-run worker commands still load env through Infisical, then
scripts/dev-agent-worker selects Podman when available, falls back to Docker,
and publishes the OpenCode server on a localhost port so the host worker can
reach the job container. Override with:
SPOON_AGENT_CONTAINER_RUNTIME=podman
SPOON_AGENT_CONTAINER_ACCESS=host_port
Production agent runtime images
Gitea CI builds and pushes three production images:
git.gbrown.org/gib/spoon-next:latest
git.gbrown.org/gib/spoon-agent-worker:latest
git.gbrown.org/gib/spoon-agent-job:latest
The worker image is the long-running service that polls Convex. The job image is the isolated workbench that the worker launches for each agent job. For the MVP, production should use the repo-provided JS/TS workbench image:
SPOON_AGENT_JOB_IMAGE="git.gbrown.org/gib/spoon-agent-job:latest"
The job image includes Node 22, Bun, pnpm and yarn through Corepack, npm, git, ripgrep, Python, build tools, OpenCode, and the Codex CLI. It is not the forked project's production runtime; it is the agent execution environment.
Production worker runtime requirements:
spoon-agent-workermust run as a separate service.- The worker needs
/var/run/docker.sockmounted so it can launch job containers. - Production should keep
SPOON_AGENT_CONTAINER_RUNTIME=dockerandSPOON_AGENT_CONTAINER_ACCESS=network. - The production Docker host must be logged into
git.gbrown.orgso worker jobs can pull the privatespoon-agent-jobimage. SPOON_WORKER_TOKENmust match the value stored in Convex production env.spoon-nextneedsSPOON_AGENT_WORKER_URL=http://spoon-agent-worker:3921andSPOON_AGENT_WORKER_INTERNAL_TOKENso Next API routes can proxy workspace file, diff, message, command, and draft PR actions.spoon-agent-workeralso needsGITHUB_APP_IDandGITHUB_APP_PRIVATE_KEY. If the private key is stored in a single-line dotenv value, encode newlines as literal\ncharacters so the worker can restore the PEM before using it.
Useful production checks:
docker login git.gbrown.org
docker pull git.gbrown.org/gib/spoon-agent-worker:latest
docker pull git.gbrown.org/gib/spoon-agent-job:latest
docker logs --tail=200 spoon-agent-worker
curl -H "Authorization: Bearer $SPOON_AGENT_WORKER_INTERNAL_TOKEN" \
http://spoon-agent-worker:3921/health
Deployment readiness checklist:
- Production Convex env has
SPOON_WORKER_TOKEN,SPOON_ENCRYPTION_KEY, GitHub App env, and Convex Auth signing keys. - Compose env has
SPOON_AGENT_WORKER_URL,SPOON_AGENT_WORKER_INTERNAL_TOKEN,SPOON_AGENT_JOB_IMAGE, and the GitHub App private key. - The production Docker host can pull private images from
git.gbrown.org. Settings -> Workerreports the expected job image, runtime, network, and active workspace count.- The first test thread uses a configured API-key provider or a trusted Codex login profile.
- If a worker restart leaves stale workspace state, use the workspace recovery
panel or
Settings -> Workercleanup.
API-key based AI provider profiles run through OpenCode. Codex ChatGPT login
profiles run through the Codex CLI: Spoon writes the encrypted auth.json into
the isolated job workspace as CODEX_HOME/.codex/auth.json before execution.
Treat that saved auth file like a password and only use it on trusted workers.
Architecture
Workspace layout
.
├── apps
│ ├── next # Next.js 16 web app and primary Spoon UI
│ ├── agent-worker # Optional OpenCode workspace / draft PR worker
│ └── expo # Expo companion app scaffold
├── packages
│ ├── backend # Convex backend package
│ │ └── convex # Schema, functions, auth, HTTP routes
│ └── ui # Shared shadcn-based UI components
├── tools # Shared lint, format, Tailwind, TS, Vitest config
├── docker # Compose files and worker/job Dockerfiles
└── scripts # Env, Convex, codegen, database, and CI helpers
Core tables
| Table | Purpose |
|---|---|
spoons |
Managed fork records |
threads |
Durable maintenance and work conversations |
threadMessages |
Messages inside threads |
syncRuns |
Upstream checks, sync attempts, and maintenance decisions |
ignoredUpstreamChanges |
Intentional ignore records that affect effective drift |
gitConnections |
Git provider connection metadata |
spoonRepositoryStates |
Latest cached upstream/fork state |
spoonCommits |
Cached upstream and fork-only commits |
spoonPullRequests |
Cached fork/upstream pull requests |
spoonSecrets |
Encrypted per-Spoon environment variables |
spoonAgentSettings |
Per-Spoon runtime, branch, command, and env-file settings |
aiProviderProfiles |
Encrypted provider/auth profiles used by OpenCode |
agentJobs |
Worker-executed workspace jobs and PR lifecycle |
agentJobEvents |
Append-only worker event log |
agentJobArtifacts |
Diffs, summaries, command output, PR body drafts |
agentWorkspaceChanges |
Recorded user, agent, and command file changes |
Important routes
| Route | Purpose |
|---|---|
/ |
Public product landing page |
/dashboard |
Maintenance overview |
/spoons |
Managed fork list |
/spoons/new |
Manual/GitHub Spoon creation |
/spoons/[spoonId] |
Spoon detail dashboard |
/spoons/[spoonId]/agent/[jobId] |
Interactive workspace |
/threads |
Global thread queue |
/threads/[threadId] |
Thread detail |
/settings/profile |
User profile settings |
/settings/integrations |
GitHub and service integration settings |
/settings/ai-providers |
AI/OpenCode provider profiles |
Legacy /updates and /agents routes redirect into /threads.
Mobile App
Current Expo scope
apps/expo is the mobile Spoon client. It is designed to mirror the core web
product without exposing worker internals or trying to turn a phone into the
primary code-editing surface.
The mobile app currently supports:
- password, GitHub, and Authentik sign-in
- Dashboard, Spoons, Threads, Workspace Review, and Settings tabs/screens
- manual Spoon creation and GitHub-assisted repository tracking
- Spoon detail views for overview, upstream commits, fork-only commits, PRs, threads, settings, clone URLs, and additional remotes
- Spoon maintenance settings, agent settings, encrypted secrets, and bulk
.envpaste import - thread list/detail, message composer, resolve/cancel actions, and workspace review links
- GitHub integration status and repository listing
- AI provider profile management, including Codex auth JSON and API-key providers
- read-only workspace review for job status, messages, diffs, events, artifacts, and draft PR links
The mobile app intentionally does not currently support:
- live workspace file browsing/editing
- mobile command execution
- direct mobile calls to the agent worker HTTP API
- mobile access to worker/container tokens
- long-running app preview stacks
- production app-store/EAS release flow
Mobile workspace editing is deferred until worker authorization and mobile editor UX are designed explicitly. For now, the phone is a strong review and control surface; the browser remains the code workspace.
Expo validation
Useful mobile checks:
bun --filter @spoon/expo lint
bun --filter @spoon/expo typecheck
bun --filter @spoon/expo test:unit
bun --filter @spoon/expo test:component
The Expo unit tests cover pure utilities such as .env parsing and formatting.
The component tests use a lightweight React Native mock layer to exercise shared
mobile controls, higher-value forms, and route smoke renders without booting a
native simulator.
Environment Reference
This project is currently private, so this section is a reference for what the application expects rather than public setup documentation.
Local Infisical account selection
Local dev and staging commands export secrets through Infisical. Spoon runs
scripts/infisical-account ensure from scripts/export-env before exporting so
machines logged into multiple Infisical accounts do not accidentally use the
wrong organization.
If your machine has only one local Infisical account, no extra setup is needed. If it has multiple accounts, create this ignored local file:
mkdir -p .local
printf "INFISICAL_EMAIL=me@gbrown.org\n" > .local/infisical.env
Log into each needed account once with infisical login. You can inspect local
profiles without printing tokens:
jq '.loggedInUsers[] | {email, domain}' ~/.infisical/infisical-config.json
.local/infisical.env supports only INFISICAL_EMAIL=... and must not be
committed. CI is unchanged; it uses injected environment files/secrets and must
not call Infisical.
Public Next variables
| Variable | Used for |
|---|---|
NEXT_PUBLIC_SITE_URL |
Canonical Spoon web URL |
NEXT_PUBLIC_CONVEX_URL |
Convex client URL |
NEXT_PUBLIC_DEPLOYMENT_URL |
Convex dashboard/deployment URL when needed |
NEXT_PUBLIC_PLAUSIBLE_URL |
Plausible analytics endpoint |
NEXT_PUBLIC_SENTRY_DSN |
Browser Sentry DSN |
NEXT_PUBLIC_SENTRY_URL |
Sentry instance URL |
NEXT_PUBLIC_SENTRY_ORG |
Sentry organization |
NEXT_PUBLIC_SENTRY_PROJECT_NAME |
Sentry project name |
Auth and email
| Variable | Used for |
|---|---|
SITE_URL |
Convex Auth site URL |
JWT_PRIVATE_KEY |
Convex Auth signing key |
JWKS |
Convex Auth JWKS |
AUTH_AUTHENTIK_ID |
Authentik OAuth client ID |
AUTH_AUTHENTIK_SECRET |
Authentik OAuth client secret |
AUTH_AUTHENTIK_ISSUER |
Authentik issuer URL |
AUTH_GITHUB_ID |
GitHub OAuth client ID |
AUTH_GITHUB_SECRET |
GitHub OAuth client secret |
USESEND_API_KEY |
UseSend API key |
USESEND_URL |
UseSend API URL |
USESEND_FROM_EMAIL |
Transactional email sender |
GitHub App
| Variable | Used for |
|---|---|
GITHUB_APP_ID |
GitHub App ID |
GITHUB_APP_CLIENT_ID |
GitHub App OAuth client ID |
GITHUB_APP_CLIENT_SECRET |
GitHub App OAuth client secret |
GITHUB_APP_PRIVATE_KEY |
GitHub App PEM private key |
GITHUB_APP_WEBHOOK_SECRET |
GitHub webhook verification secret |
GITHUB_APP_SLUG |
GitHub App slug |
GITHUB_APP_INSTALLATION_ID |
Default/local installation ID |
GITHUB_APP_OWNER |
Default/local installation owner |
Convex, storage, and runtime
| Variable | Used for |
|---|---|
CONVEX_SELF_HOSTED_URL |
Self-hosted Convex API URL |
CONVEX_SELF_HOSTED_ADMIN_KEY |
Admin key for deploying/syncing Convex |
CONVEX_CLOUD_ORIGIN |
Convex backend origin |
CONVEX_SITE_ORIGIN |
Convex site-function origin |
CONVEX_SITE_URL |
Site URL seen by Convex Auth |
POSTGRES_URL |
Convex storage database URL |
SPOON_ENCRYPTION_KEY |
Encryption key for stored secrets/provider auth |
SPOON_WORKER_TOKEN |
Worker token for Convex worker mutations |
SPOON_AGENT_WORKER_URL |
Internal worker HTTP URL used by Next |
SPOON_AGENT_WORKER_HTTP_PORT |
Worker HTTP port |
SPOON_AGENT_WORKER_INTERNAL_TOKEN |
Server-only token for Next-to-worker proxy |
SPOON_AGENT_JOB_IMAGE |
Agent job container image |
SPOON_AGENT_RUNTIME |
Runtime mode, currently Docker/Podman-oriented |
SPOON_AGENT_CONTAINER_RUNTIME |
Container CLI used by worker, docker/podman |
SPOON_AGENT_CONTAINER_ACCESS |
network in prod, host_port for host dev |
SPOON_AGENT_MAX_CONCURRENT_JOBS |
Worker concurrency limit |
SPOON_AGENT_JOB_TIMEOUT_MS |
Job timeout |
SPOON_AGENT_WORKDIR |
Worker work directory |
SPOON_AGENT_HOST_WORKDIR |
Host path matching SPOON_AGENT_WORKDIR when the worker runs in Docker and controls the host Docker socket |
SPOON_AGENT_NETWORK |
Optional job container network |
Deployment and observability
| Variable | Used for |
|---|---|
NODE_ENV |
Runtime environment |
SENTRY_AUTH_TOKEN |
Sentry source map/upload auth |
REDACT_LOGS_TO_CLIENT |
Convex log redaction setting |
DISABLE_BEACON |
Self-hosted Convex beacon setting |
DO_NOT_REQUIRE_SSL |
Self-hosted Convex SSL behavior |
CI_ENV_FILE |
CI-provided env file path |
Current Status
Implemented
- Thread-first Next.js product shell
- GitHub App connection and fork creation foundation
- GitHub drift refresh, commit cache, PR cache, and sync-run history
- Effective drift and ignored upstream change records
- Global Threads page and Spoon-scoped Threads tab
- OpenCode/Codex-oriented agent worker and browser workspace foundation
- Monaco editor with optional Vim mode
- Diff viewer, command panel, worker logs, and artifacts
- Encrypted Spoon secrets and bulk
.envimport - Encrypted AI provider profiles, including Codex auth JSON and API-key provider support
- Authentik, GitHub, and password auth through Convex Auth
- Self-hosted Convex/Postgres deployment model
Intentionally not done yet
- Autonomous merging for custom/diverged forks
- Non-GitHub provider automation
- Pushing agent branches to additional remotes
- Long-running preview stacks for arbitrary forked projects
- Direct browser access to worker containers
- Public self-hosting setup documentation
- Production mobile release flow
Notes
Spoon is built for a very specific maintenance problem: "I want to fork this project, but I do not want to permanently become its maintenance team."
The current product direction is to make that maintenance visible, threaded, reviewable, and increasingly automated where it is safe. Clean forks can stay close automatically. Custom forks get context, workspace help, and draft PRs.