diff --git a/.dockerignore b/.dockerignore index cb03545..5625bc9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,7 +17,7 @@ coverage/ # Environment .env .env.* -.openhandoff/ +.sandbox-agent-factory/ # IDE .idea/ diff --git a/.gitignore b/.gitignore index da6874a..c8153ca 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,7 @@ Cargo.lock # Example temp files .tmp-upload/ *.db -.openhandoff/ +.sandbox-agent-factory/ # CLI binaries (downloaded during npm publish) sdks/cli/platforms/*/bin/ diff --git a/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite new file mode 100644 index 0000000..159e90a Binary files /dev/null and b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite differ diff --git a/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm new file mode 100644 index 0000000..d319f4d Binary files /dev/null and b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm differ diff --git a/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal new file mode 100644 index 0000000..3777140 Binary files /dev/null and b/.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal differ diff --git a/.openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite b/.openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite new file mode 100644 index 0000000..4dd06a1 Binary files /dev/null and b/.openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite differ diff --git a/.openhandoff/backend/sqlite/history/83fb73090cf32cca.sqlite b/.openhandoff/backend/sqlite/history/83fb73090cf32cca.sqlite new file mode 100644 index 0000000..9b4d150 Binary files /dev/null and b/.openhandoff/backend/sqlite/history/83fb73090cf32cca.sqlite differ diff --git a/.openhandoff/backend/sqlite/project/27bd62398cfef512.sqlite b/.openhandoff/backend/sqlite/project/27bd62398cfef512.sqlite new file mode 100644 index 0000000..2ad3be7 Binary files /dev/null and b/.openhandoff/backend/sqlite/project/27bd62398cfef512.sqlite differ diff --git a/.openhandoff/backend/sqlite/project/6d0adb3e91e6d8af.sqlite b/.openhandoff/backend/sqlite/project/6d0adb3e91e6d8af.sqlite new file mode 100644 index 0000000..2ad3be7 Binary files /dev/null and b/.openhandoff/backend/sqlite/project/6d0adb3e91e6d8af.sqlite differ diff --git a/.openhandoff/backend/sqlite/project/86bea07dcf41d624.sqlite b/.openhandoff/backend/sqlite/project/86bea07dcf41d624.sqlite new file mode 100644 index 0000000..6b686ea Binary files /dev/null and b/.openhandoff/backend/sqlite/project/86bea07dcf41d624.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite new file mode 100644 index 0000000..159e90a Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-shm new file mode 100644 index 0000000..8f433e6 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-shm differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-wal b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-wal new file mode 100644 index 0000000..eed2dd4 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/040d32046643c32d.sqlite-wal differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/33da3db915764968.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/33da3db915764968.sqlite new file mode 100644 index 0000000..8614625 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/33da3db915764968.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/3715079ac70cc298.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/3715079ac70cc298.sqlite new file mode 100644 index 0000000..8614625 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/3715079ac70cc298.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite new file mode 100644 index 0000000..159e90a Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-shm new file mode 100644 index 0000000..b0453eb Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-shm differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-wal b/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-wal new file mode 100644 index 0000000..7ec0a9a Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/44352c1fd428a7da.sqlite-wal differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/5c43c54a5a4efcbd.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/5c43c54a5a4efcbd.sqlite new file mode 100644 index 0000000..8614625 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/5c43c54a5a4efcbd.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite new file mode 100644 index 0000000..159e90a Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-shm new file mode 100644 index 0000000..5479a93 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-shm differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-wal b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-wal new file mode 100644 index 0000000..3beca89 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/70b6da466222ac43.sqlite-wal differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/9ed3b93c064512c4.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/9ed3b93c064512c4.sqlite new file mode 100644 index 0000000..8614625 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/9ed3b93c064512c4.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/afbe363197a91921.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/afbe363197a91921.sqlite new file mode 100644 index 0000000..8614625 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/afbe363197a91921.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/d06bdd0ae7a34f37.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/d06bdd0ae7a34f37.sqlite new file mode 100644 index 0000000..8614625 Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/d06bdd0ae7a34f37.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite b/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite new file mode 100644 index 0000000..159e90a Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-shm b/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-shm new file mode 100644 index 0000000..5f6c5df Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-shm differ diff --git a/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-wal b/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-wal new file mode 100644 index 0000000..b39c5eb Binary files /dev/null and b/.openhandoff/backend/sqlite/sandbox-instance/e3f12a890eaebaac.sqlite-wal differ diff --git a/.openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite b/.openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite new file mode 100644 index 0000000..0a73908 Binary files /dev/null and b/.openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite differ diff --git a/.openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite b/.openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite new file mode 100644 index 0000000..73824cb Binary files /dev/null and b/.openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite differ diff --git a/factory/CLAUDE.md b/factory/CLAUDE.md index 689b33c..c34c64c 100644 --- a/factory/CLAUDE.md +++ b/factory/CLAUDE.md @@ -27,7 +27,7 @@ Use `pnpm` workspaces and Turborepo. - `packages/cli` is fully disabled for active development. - Do not implement new behavior in `packages/cli` unless explicitly requested. - Frontend is the primary product surface; prioritize `packages/frontend` + supporting `packages/client`/`packages/backend`. -- Workspace `build`, `typecheck`, and `test` intentionally exclude `@openhandoff/cli`. +- Workspace `build`, `typecheck`, and `test` intentionally exclude `@sandbox-agent/factory-cli`. - `pnpm-workspace.yaml` excludes `packages/cli` from workspace package resolution. ## Common Commands @@ -37,8 +37,8 @@ Use `pnpm` workspaces and Turborepo. - Start the full dev stack: `just factory-dev` - Start the local production-build preview stack: `just factory-preview` - Start only the backend locally: `just factory-backend-start` -- Start only the frontend locally: `pnpm --filter @openhandoff/frontend dev` -- Start the frontend against the mock workbench client: `OPENHANDOFF_FRONTEND_CLIENT_MODE=mock pnpm --filter @openhandoff/frontend dev` +- Start only the frontend locally: `pnpm --filter @sandbox-agent/factory-frontend dev` +- Start the frontend against the mock workbench client: `FACTORY_FRONTEND_CLIENT_MODE=mock pnpm --filter @sandbox-agent/factory-frontend dev` - Stop the compose dev stack: `just factory-dev-down` - Tail compose logs: `just factory-dev-logs` - Stop the preview stack: `just factory-preview-down` @@ -85,10 +85,12 @@ For all Rivet/RivetKit implementation: 5. RivetKit is linked via pnpm `link:` protocol to `../rivet/rivetkit-typescript/packages/rivetkit`. Sub-packages (`@rivetkit/sqlite-vfs`, etc.) resolve transitively from the rivet workspace. - Dedicated local checkout for this workspace: `/Users/nathan/conductor/workspaces/handoff/rivet-checkout` - Dev worktree note: when working on RivetKit fixes for this repo, prefer the dedicated local checkout above and link to `../rivet-checkout/rivetkit-typescript/packages/rivetkit`. -6. Before using, build RivetKit in the rivet repo: + - If Docker dev needs a different host path, export `HF_RIVET_CHECKOUT_PATH=/abs/path/to/rivet-checkout` before `docker compose -f factory/compose.dev.yaml up`. +6. Before using a fresh Rivet checkout, generate RivetKit schemas and build RivetKit in the rivet repo: ```bash cd ../rivet-checkout/rivetkit-typescript pnpm install + pnpm --dir packages/rivetkit build:schema pnpm build -F rivetkit ``` @@ -132,7 +134,7 @@ For all Rivet/RivetKit implementation: - Workspace resolution order: `--workspace` flag -> config default -> `"default"`. - `ControlPlaneActor` is replaced by `WorkspaceActor` (workspace coordinator). - Every actor key must be prefixed with workspace namespace (`["ws", workspaceId, ...]`). -- CLI/TUI/GUI must use `@openhandoff/client` (`packages/client`) for backend access; `rivetkit/client` imports are only allowed inside `packages/client`. +- CLI/TUI/GUI must use `@sandbox-agent/factory-client` (`packages/client`) for backend access; `rivetkit/client` imports are only allowed inside `packages/client`. - Do not add custom backend REST endpoints (no `/v1/*` shim layer). - We own the sandbox-agent project; treat sandbox-agent defects as first-party bugs and fix them instead of working around them. - Keep strict single-writer ownership: each table/row has exactly one actor writer. @@ -144,7 +146,7 @@ For all Rivet/RivetKit implementation: - Use create semantics only on explicit provisioning/create paths where creating a new actor instance is intended. - `getOrCreate` is a last resort for create paths when an explicit create API is unavailable; never use it in read/command paths. - For long-lived cross-actor links (for example sandbox/session runtime access), persist actor identity (`actorId`) and keep a fallback lookup path by actor id. -- Docker dev: `compose.dev.yaml` mounts a named volume at `/root/.local/share/openhandoff/repos` to persist backend-managed git clones across restarts. Code must still work if this volume is not present (create directories as needed). +- Docker dev: `compose.dev.yaml` mounts a named volume at `/root/.local/share/sandbox-agent-factory/repos` to persist backend-managed git clones across restarts. Code must still work if this volume is not present (create directories as needed). - RivetKit actor `c.state` is durable, but in Docker it is stored under `/root/.local/share/rivetkit`. If that path is not persisted, actor state-derived indexes (for example, in `project` actor state) can be lost after container recreation even when other data still exists. - Workflow history divergence policy: - Production: never auto-delete actor state to resolve `HistoryDivergedError`; ship explicit workflow migrations (`ctx.removed(...)`, step compatibility). @@ -167,7 +169,7 @@ For all Rivet/RivetKit implementation: ## Config -- Keep config path at `~/.config/openhandoff/config.toml`. +- Keep config path at `~/.config/sandbox-agent-factory/config.toml`. - Evolve properties in place; do not move config location. ## Project Guidance diff --git a/factory/CONTRIBUTING.md b/factory/CONTRIBUTING.md index 759f348..04a9e2d 100644 --- a/factory/CONTRIBUTING.md +++ b/factory/CONTRIBUTING.md @@ -5,8 +5,8 @@ 1. Clone: ```bash -git clone https://github.com/rivet-dev/openhandoff.git -cd openhandoff +git clone https://github.com/rivet-dev/sandbox-agent-factory.git +cd sandbox-agent-factory ``` 2. Install dependencies: @@ -35,7 +35,7 @@ Build local RivetKit before backend changes that depend on Rivet internals: cd ../rivet pnpm build -F rivetkit -cd /path/to/openhandoff +cd /path/to/sandbox-agent-factory just sync-rivetkit ``` diff --git a/factory/Dockerfile b/factory/Dockerfile index 5693650..8d5dfe9 100644 --- a/factory/Dockerfile +++ b/factory/Dockerfile @@ -22,15 +22,15 @@ COPY packages/rivetkit-vendor/sqlite-vfs-win32-x64/package.json packages/rivetki COPY packages/rivetkit-vendor/runner/package.json packages/rivetkit-vendor/runner/package.json COPY packages/rivetkit-vendor/runner-protocol/package.json packages/rivetkit-vendor/runner-protocol/package.json COPY packages/rivetkit-vendor/virtual-websocket/package.json packages/rivetkit-vendor/virtual-websocket/package.json -RUN pnpm fetch --frozen-lockfile --filter @openhandoff/backend... +RUN pnpm fetch --frozen-lockfile --filter @sandbox-agent/factory-backend... FROM base AS build COPY --from=deps /pnpm/store /pnpm/store COPY . . -RUN pnpm install --frozen-lockfile --prefer-offline --filter @openhandoff/backend... -RUN pnpm --filter @openhandoff/shared build -RUN pnpm --filter @openhandoff/backend build -RUN pnpm --filter @openhandoff/backend deploy --prod --legacy /out +RUN pnpm install --frozen-lockfile --prefer-offline --filter @sandbox-agent/factory-backend... +RUN pnpm --filter @sandbox-agent/factory-shared build +RUN pnpm --filter @sandbox-agent/factory-backend build +RUN pnpm --filter @sandbox-agent/factory-backend deploy --prod --legacy /out FROM oven/bun:1.2 AS runtime ENV NODE_ENV=production diff --git a/factory/README.md b/factory/README.md index c49f7cb..a385089 100644 --- a/factory/README.md +++ b/factory/README.md @@ -1,9 +1,7 @@ -# OpenHandoff +# Sandbox Agent Factory TypeScript workspace handoff system powered by RivetKit actors, SQLite/Drizzle state, and OpenTUI. -**Documentation**: [openhandoff.dev](https://openhandoff.dev) - ## Quick Install ```bash diff --git a/factory/compose.dev.yaml b/factory/compose.dev.yaml index 80fea12..e4ec7a3 100644 --- a/factory/compose.dev.yaml +++ b/factory/compose.dev.yaml @@ -1,17 +1,17 @@ -name: openhandoff +name: sandbox-agent-factory services: backend: build: context: .. dockerfile: factory/docker/backend.dev.Dockerfile - image: openhandoff-backend-dev + image: sandbox-agent-factory-backend-dev working_dir: /app environment: HF_BACKEND_HOST: "0.0.0.0" HF_BACKEND_PORT: "7741" HF_RIVET_MANAGER_PORT: "8750" - RIVETKIT_STORAGE_PATH: "/root/.local/share/openhandoff/rivetkit" + RIVETKIT_STORAGE_PATH: "/root/.local/share/sandbox-agent-factory/rivetkit" # Pass through credentials needed for agent execution + PR creation in dev/e2e. # Do not hardcode secrets; set these in your environment when starting compose. ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" @@ -32,21 +32,21 @@ services: - "8750:8750" volumes: - "..:/app" - # The linked RivetKit checkout resolves from factory packages to /handoff/rivet-checkout in-container. - - "../../../handoff/rivet-checkout:/handoff/rivet-checkout:ro" + # Override HF_RIVET_CHECKOUT_PATH when the linked Rivet workspace lives outside the default sibling checkout. + - "${HF_RIVET_CHECKOUT_PATH:-../../../handoff/rivet-checkout}:/handoff/rivet-checkout:ro" # Reuse the host Codex auth profile for local sandbox-agent Codex sessions in dev. - "${HOME}/.codex:/root/.codex" # Keep backend dependency installs Linux-native instead of using host node_modules. - - "openhandoff_backend_root_node_modules:/app/node_modules" - - "openhandoff_backend_backend_node_modules:/app/factory/packages/backend/node_modules" - - "openhandoff_backend_shared_node_modules:/app/factory/packages/shared/node_modules" - - "openhandoff_backend_persist_rivet_node_modules:/app/sdks/persist-rivet/node_modules" - - "openhandoff_backend_typescript_node_modules:/app/sdks/typescript/node_modules" - - "openhandoff_backend_pnpm_store:/root/.local/share/pnpm/store" + - "sandbox-agent-factory_backend_root_node_modules:/app/node_modules" + - "sandbox-agent-factory_backend_backend_node_modules:/app/factory/packages/backend/node_modules" + - "sandbox-agent-factory_backend_shared_node_modules:/app/factory/packages/shared/node_modules" + - "sandbox-agent-factory_backend_persist_rivet_node_modules:/app/sdks/persist-rivet/node_modules" + - "sandbox-agent-factory_backend_typescript_node_modules:/app/sdks/typescript/node_modules" + - "sandbox-agent-factory_backend_pnpm_store:/root/.local/share/pnpm/store" # Persist backend-managed local git clones across container restarts. - - "openhandoff_git_repos:/root/.local/share/openhandoff/repos" + - "sandbox-agent-factory_git_repos:/root/.local/share/sandbox-agent-factory/repos" # Persist RivetKit local storage across container restarts. - - "openhandoff_rivetkit_storage:/root/.local/share/openhandoff/rivetkit" + - "sandbox-agent-factory_rivetkit_storage:/root/.local/share/sandbox-agent-factory/rivetkit" frontend: build: @@ -62,29 +62,29 @@ services: - "4173:4173" volumes: - "..:/app" - # Ensure logs in .openhandoff/ persist on the host even if we change source mounts later. - - "./.openhandoff:/app/factory/.openhandoff" - - "../../../handoff/rivet-checkout:/handoff/rivet-checkout:ro" + # Ensure logs in .sandbox-agent-factory/ persist on the host even if we change source mounts later. + - "./.sandbox-agent-factory:/app/factory/.sandbox-agent-factory" + - "${HF_RIVET_CHECKOUT_PATH:-../../../handoff/rivet-checkout}:/handoff/rivet-checkout:ro" # Use Linux-native workspace dependencies inside the container instead of host node_modules. - - "openhandoff_node_modules:/app/node_modules" - - "openhandoff_client_node_modules:/app/factory/packages/client/node_modules" - - "openhandoff_frontend_errors_node_modules:/app/factory/packages/frontend-errors/node_modules" - - "openhandoff_frontend_node_modules:/app/factory/packages/frontend/node_modules" - - "openhandoff_shared_node_modules:/app/factory/packages/shared/node_modules" - - "openhandoff_pnpm_store:/tmp/.local/share/pnpm/store" + - "sandbox-agent-factory_node_modules:/app/node_modules" + - "sandbox-agent-factory_client_node_modules:/app/factory/packages/client/node_modules" + - "sandbox-agent-factory_frontend_errors_node_modules:/app/factory/packages/frontend-errors/node_modules" + - "sandbox-agent-factory_frontend_node_modules:/app/factory/packages/frontend/node_modules" + - "sandbox-agent-factory_shared_node_modules:/app/factory/packages/shared/node_modules" + - "sandbox-agent-factory_pnpm_store:/tmp/.local/share/pnpm/store" volumes: - openhandoff_backend_root_node_modules: {} - openhandoff_backend_backend_node_modules: {} - openhandoff_backend_shared_node_modules: {} - openhandoff_backend_persist_rivet_node_modules: {} - openhandoff_backend_typescript_node_modules: {} - openhandoff_backend_pnpm_store: {} - openhandoff_git_repos: {} - openhandoff_rivetkit_storage: {} - openhandoff_node_modules: {} - openhandoff_client_node_modules: {} - openhandoff_frontend_errors_node_modules: {} - openhandoff_frontend_node_modules: {} - openhandoff_shared_node_modules: {} - openhandoff_pnpm_store: {} + sandbox-agent-factory_backend_root_node_modules: {} + sandbox-agent-factory_backend_backend_node_modules: {} + sandbox-agent-factory_backend_shared_node_modules: {} + sandbox-agent-factory_backend_persist_rivet_node_modules: {} + sandbox-agent-factory_backend_typescript_node_modules: {} + sandbox-agent-factory_backend_pnpm_store: {} + sandbox-agent-factory_git_repos: {} + sandbox-agent-factory_rivetkit_storage: {} + sandbox-agent-factory_node_modules: {} + sandbox-agent-factory_client_node_modules: {} + sandbox-agent-factory_frontend_errors_node_modules: {} + sandbox-agent-factory_frontend_node_modules: {} + sandbox-agent-factory_shared_node_modules: {} + sandbox-agent-factory_pnpm_store: {} diff --git a/factory/compose.preview.yaml b/factory/compose.preview.yaml index 88cdad3..01bbe93 100644 --- a/factory/compose.preview.yaml +++ b/factory/compose.preview.yaml @@ -1,16 +1,16 @@ -name: openhandoff-preview +name: sandbox-agent-factory-preview services: backend: build: context: .. dockerfile: quebec/docker/backend.preview.Dockerfile - image: openhandoff-backend-preview + image: sandbox-agent-factory-backend-preview environment: HF_BACKEND_HOST: "0.0.0.0" HF_BACKEND_PORT: "7841" HF_RIVET_MANAGER_PORT: "8850" - RIVETKIT_STORAGE_PATH: "/root/.local/share/openhandoff/rivetkit" + RIVETKIT_STORAGE_PATH: "/root/.local/share/sandbox-agent-factory/rivetkit" ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" CLAUDE_API_KEY: "${CLAUDE_API_KEY:-${ANTHROPIC_API_KEY:-}}" OPENAI_API_KEY: "${OPENAI_API_KEY:-}" @@ -26,19 +26,19 @@ services: - "8850:8850" volumes: - "${HOME}/.codex:/root/.codex" - - "openhandoff_preview_git_repos:/root/.local/share/openhandoff/repos" - - "openhandoff_preview_rivetkit_storage:/root/.local/share/openhandoff/rivetkit" + - "sandbox-agent-factory_preview_git_repos:/root/.local/share/sandbox-agent-factory/repos" + - "sandbox-agent-factory_preview_rivetkit_storage:/root/.local/share/sandbox-agent-factory/rivetkit" frontend: build: context: .. dockerfile: quebec/docker/frontend.preview.Dockerfile - image: openhandoff-frontend-preview + image: sandbox-agent-factory-frontend-preview depends_on: - backend ports: - "4273:4273" volumes: - openhandoff_preview_git_repos: {} - openhandoff_preview_rivetkit_storage: {} + sandbox-agent-factory_preview_git_repos: {} + sandbox-agent-factory_preview_rivetkit_storage: {} diff --git a/factory/docker/backend.dev.Dockerfile b/factory/docker/backend.dev.Dockerfile index a53e018..fb84e70 100644 --- a/factory/docker/backend.dev.Dockerfile +++ b/factory/docker/backend.dev.Dockerfile @@ -39,4 +39,4 @@ ENV SANDBOX_AGENT_BIN="/root/.local/bin/sandbox-agent" WORKDIR /app -CMD ["bash", "-lc", "git config --global --add safe.directory /app >/dev/null 2>&1 || true; pnpm install --force --frozen-lockfile --filter @openhandoff/backend... && exec bun factory/packages/backend/src/index.ts start --host 0.0.0.0 --port 7741"] +CMD ["bash", "-lc", "git config --global --add safe.directory /app >/dev/null 2>&1 || true; pnpm install --force --frozen-lockfile --filter @sandbox-agent/factory-backend... && exec bun factory/packages/backend/src/index.ts start --host 0.0.0.0 --port 7741"] diff --git a/factory/docker/backend.preview.Dockerfile b/factory/docker/backend.preview.Dockerfile index 3ea5aa8..dbadf36 100644 --- a/factory/docker/backend.preview.Dockerfile +++ b/factory/docker/backend.preview.Dockerfile @@ -42,8 +42,8 @@ COPY quebec /workspace/quebec COPY rivet-checkout /workspace/rivet-checkout RUN pnpm install --frozen-lockfile -RUN pnpm --filter @openhandoff/shared build -RUN pnpm --filter @openhandoff/client build -RUN pnpm --filter @openhandoff/backend build +RUN pnpm --filter @sandbox-agent/factory-shared build +RUN pnpm --filter @sandbox-agent/factory-client build +RUN pnpm --filter @sandbox-agent/factory-backend build CMD ["bash", "-lc", "git config --global --add safe.directory /workspace/quebec >/dev/null 2>&1 || true; exec bun packages/backend/dist/index.js start --host 0.0.0.0 --port 7841"] diff --git a/factory/docker/frontend.dev.Dockerfile b/factory/docker/frontend.dev.Dockerfile index 057b88d..eb52d2e 100644 --- a/factory/docker/frontend.dev.Dockerfile +++ b/factory/docker/frontend.dev.Dockerfile @@ -8,4 +8,4 @@ RUN npm install -g pnpm@10.28.2 WORKDIR /app -CMD ["bash", "-lc", "pnpm install --force --frozen-lockfile --filter @openhandoff/frontend... && cd factory/packages/frontend && exec pnpm vite --host 0.0.0.0 --port 4173"] +CMD ["bash", "-lc", "pnpm install --force --frozen-lockfile --filter @sandbox-agent/factory-frontend... && cd factory/packages/frontend && exec pnpm vite --host 0.0.0.0 --port 4173"] diff --git a/factory/docker/frontend.preview.Dockerfile b/factory/docker/frontend.preview.Dockerfile index 7f90b2a..aaf3ae0 100644 --- a/factory/docker/frontend.preview.Dockerfile +++ b/factory/docker/frontend.preview.Dockerfile @@ -10,10 +10,10 @@ COPY quebec /workspace/quebec COPY rivet-checkout /workspace/rivet-checkout RUN pnpm install --frozen-lockfile -RUN pnpm --filter @openhandoff/shared build -RUN pnpm --filter @openhandoff/client build -RUN pnpm --filter @openhandoff/frontend-errors build -RUN pnpm --filter @openhandoff/frontend build +RUN pnpm --filter @sandbox-agent/factory-shared build +RUN pnpm --filter @sandbox-agent/factory-client build +RUN pnpm --filter @sandbox-agent/factory-frontend-errors build +RUN pnpm --filter @sandbox-agent/factory-frontend build FROM nginx:1.27-alpine diff --git a/factory/packages/backend/package.json b/factory/packages/backend/package.json index 68c514c..4427b88 100644 --- a/factory/packages/backend/package.json +++ b/factory/packages/backend/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/backend", + "name": "@sandbox-agent/factory-backend", "version": "0.1.0", "private": true, "type": "module", @@ -17,7 +17,7 @@ "@hono/node-server": "^1.19.7", "@hono/node-ws": "^1.3.0", "@iarna/toml": "^2.2.5", - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "@sandbox-agent/persist-rivet": "workspace:*", "drizzle-orm": "^0.44.5", "hono": "^4.11.9", diff --git a/factory/packages/backend/src/actors/context.ts b/factory/packages/backend/src/actors/context.ts index 3a7a875..954a22a 100644 --- a/factory/packages/backend/src/actors/context.ts +++ b/factory/packages/backend/src/actors/context.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import type { BackendDriver } from "../driver.js"; import type { NotificationService } from "../notifications/index.js"; import type { ProviderRegistry } from "../providers/index.js"; diff --git a/factory/packages/backend/src/actors/events.ts b/factory/packages/backend/src/actors/events.ts index 8f9ea28..958b105 100644 --- a/factory/packages/backend/src/actors/events.ts +++ b/factory/packages/backend/src/actors/events.ts @@ -1,4 +1,4 @@ -import type { HandoffStatus, ProviderId } from "@openhandoff/shared"; +import type { HandoffStatus, ProviderId } from "@sandbox-agent/factory-shared"; export interface HandoffCreatedEvent { workspaceId: string; diff --git a/factory/packages/backend/src/actors/handles.ts b/factory/packages/backend/src/actors/handles.ts index a05a7fb..8f06f6a 100644 --- a/factory/packages/backend/src/actors/handles.ts +++ b/factory/packages/backend/src/actors/handles.ts @@ -8,7 +8,7 @@ import { sandboxInstanceKey, workspaceKey } from "./keys.js"; -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; export function actorClient(c: any) { return c.client(); diff --git a/factory/packages/backend/src/actors/handoff-status-sync/index.ts b/factory/packages/backend/src/actors/handoff-status-sync/index.ts index 86c8b3d..296db21 100644 --- a/factory/packages/backend/src/actors/handoff-status-sync/index.ts +++ b/factory/packages/backend/src/actors/handoff-status-sync/index.ts @@ -1,6 +1,6 @@ import { actor, queue } from "rivetkit"; import { workflow } from "rivetkit/workflow"; -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; import { getHandoff, getSandboxInstance, selfHandoffStatusSync } from "../handles.js"; import { logActorWarning, resolveErrorMessage, resolveErrorStack } from "../logging.js"; import { type PollingControlState, runWorkflowPollingLoop } from "../polling.js"; diff --git a/factory/packages/backend/src/actors/handoff/index.ts b/factory/packages/backend/src/actors/handoff/index.ts index 2ad52f7..4715ad0 100644 --- a/factory/packages/backend/src/actors/handoff/index.ts +++ b/factory/packages/backend/src/actors/handoff/index.ts @@ -10,7 +10,7 @@ import type { HandoffWorkbenchSendMessageInput, HandoffWorkbenchUpdateDraftInput, ProviderId -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { expectQueueResponse } from "../../services/queue.js"; import { selfHandoff } from "../handles.js"; import { handoffDb } from "./db/db.js"; diff --git a/factory/packages/backend/src/actors/handoff/workflow/common.ts b/factory/packages/backend/src/actors/handoff/workflow/common.ts index 45c1df6..f517e11 100644 --- a/factory/packages/backend/src/actors/handoff/workflow/common.ts +++ b/factory/packages/backend/src/actors/handoff/workflow/common.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { eq } from "drizzle-orm"; -import type { HandoffRecord, HandoffStatus } from "@openhandoff/shared"; +import type { HandoffRecord, HandoffStatus } from "@sandbox-agent/factory-shared"; import { getOrCreateWorkspace } from "../../handles.js"; import { handoff as handoffTable, handoffRuntime, handoffSandboxes } from "../db/schema.js"; import { historyKey } from "../../keys.js"; diff --git a/factory/packages/backend/src/actors/handoff/workflow/index.ts b/factory/packages/backend/src/actors/handoff/workflow/index.ts index 7c090f9..a21d32a 100644 --- a/factory/packages/backend/src/actors/handoff/workflow/index.ts +++ b/factory/packages/backend/src/actors/handoff/workflow/index.ts @@ -46,6 +46,8 @@ import { export { HANDOFF_QUEUE_NAMES, handoffWorkflowQueueName } from "./queue.js"; +const INIT_ENSURE_NAME_TIMEOUT_MS = 5 * 60_000; + type HandoffQueueName = (typeof HANDOFF_QUEUE_NAMES)[number]; type WorkflowHandler = (loopCtx: any, msg: { name: HandoffQueueName; body: any; complete: (response: unknown) => Promise }) => Promise; @@ -75,7 +77,11 @@ const commandHandlers: Record = { const body = msg.body; await loopCtx.removed("init-failed", "step"); try { - await loopCtx.step("init-ensure-name", async () => initEnsureNameActivity(loopCtx)); + await loopCtx.step({ + name: "init-ensure-name", + timeout: INIT_ENSURE_NAME_TIMEOUT_MS, + run: async () => initEnsureNameActivity(loopCtx), + }); await loopCtx.step("init-assert-name", async () => initAssertNameActivity(loopCtx)); const sandbox = await loopCtx.step({ diff --git a/factory/packages/backend/src/actors/history/index.ts b/factory/packages/backend/src/actors/history/index.ts index 15e7ca5..e051fc7 100644 --- a/factory/packages/backend/src/actors/history/index.ts +++ b/factory/packages/backend/src/actors/history/index.ts @@ -2,7 +2,7 @@ import { and, desc, eq } from "drizzle-orm"; import { actor, queue } from "rivetkit"; import { Loop, workflow } from "rivetkit/workflow"; -import type { HistoryEvent } from "@openhandoff/shared"; +import type { HistoryEvent } from "@sandbox-agent/factory-shared"; import { selfHistory } from "../handles.js"; import { historyDb } from "./db/db.js"; import { events } from "./db/schema.js"; diff --git a/factory/packages/backend/src/actors/logging.ts b/factory/packages/backend/src/actors/logging.ts index ffc45ab..8f43f72 100644 --- a/factory/packages/backend/src/actors/logging.ts +++ b/factory/packages/backend/src/actors/logging.ts @@ -27,5 +27,5 @@ export function logActorWarning( ...(context ?? {}) }; // eslint-disable-next-line no-console - console.warn("[openhandoff][actor:warn]", payload); + console.warn("[factory][actor:warn]", payload); } diff --git a/factory/packages/backend/src/actors/project/actions.ts b/factory/packages/backend/src/actors/project/actions.ts index d0fc978..fab3dfc 100644 --- a/factory/packages/backend/src/actors/project/actions.ts +++ b/factory/packages/backend/src/actors/project/actions.ts @@ -10,7 +10,7 @@ import type { RepoOverview, RepoStackAction, RepoStackActionResult -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { getActorRuntimeContext } from "../context.js"; import { getHandoff, @@ -21,7 +21,7 @@ import { selfProject } from "../handles.js"; import { isActorNotFoundError, logActorWarning, resolveErrorMessage } from "../logging.js"; -import { openhandoffRepoClonePath } from "../../services/openhandoff-paths.js"; +import { factoryRepoClonePath } from "../../services/factory-paths.js"; import { expectQueueResponse } from "../../services/queue.js"; import { withRepoGitLock } from "../../services/repo-git-lock.js"; import { branches, handoffIndex, prCache, repoMeta } from "./db/schema.js"; @@ -125,7 +125,7 @@ export function projectWorkflowQueueName(name: ProjectQueueName): ProjectQueueNa async function ensureLocalClone(c: any, remoteUrl: string): Promise { const { config, driver } = getActorRuntimeContext(); - const localPath = openhandoffRepoClonePath(config, c.state.workspaceId, c.state.repoId); + const localPath = factoryRepoClonePath(config, c.state.workspaceId, c.state.repoId); await driver.git.ensureCloned(remoteUrl, localPath); c.state.localPath = localPath; return localPath; diff --git a/factory/packages/backend/src/actors/sandbox-instance/index.ts b/factory/packages/backend/src/actors/sandbox-instance/index.ts index e20b86a..5a53647 100644 --- a/factory/packages/backend/src/actors/sandbox-instance/index.ts +++ b/factory/packages/backend/src/actors/sandbox-instance/index.ts @@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises"; import { eq } from "drizzle-orm"; import { actor, queue } from "rivetkit"; import { Loop, workflow } from "rivetkit/workflow"; -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; import type { SessionEvent, SessionRecord } from "sandbox-agent"; import { sandboxInstanceDb } from "./db/db.js"; import { sandboxInstance as sandboxInstanceTable } from "./db/schema.js"; diff --git a/factory/packages/backend/src/actors/workspace/actions.ts b/factory/packages/backend/src/actors/workspace/actions.ts index 93acf16..7c41ffe 100644 --- a/factory/packages/backend/src/actors/workspace/actions.ts +++ b/factory/packages/backend/src/actors/workspace/actions.ts @@ -27,7 +27,7 @@ import type { RepoRecord, SwitchResult, WorkspaceUseInput -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { getActorRuntimeContext } from "../context.js"; import { getHandoff, getOrCreateHistory, getOrCreateProject, selfWorkspace } from "../handles.js"; import { logActorWarning, resolveErrorMessage } from "../logging.js"; diff --git a/factory/packages/backend/src/config/backend.ts b/factory/packages/backend/src/config/backend.ts index 66ac3f1..4c1bd18 100644 --- a/factory/packages/backend/src/config/backend.ts +++ b/factory/packages/backend/src/config/backend.ts @@ -2,9 +2,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; import { homedir } from "node:os"; import * as toml from "@iarna/toml"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; -export const CONFIG_PATH = `${homedir()}/.config/openhandoff/config.toml`; +export const CONFIG_PATH = `${homedir()}/.config/sandbox-agent-factory/config.toml`; export function loadConfig(path = CONFIG_PATH): AppConfig { if (!existsSync(path)) { diff --git a/factory/packages/backend/src/config/workspace.ts b/factory/packages/backend/src/config/workspace.ts index a7b4010..4937bdc 100644 --- a/factory/packages/backend/src/config/workspace.ts +++ b/factory/packages/backend/src/config/workspace.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; export function defaultWorkspace(config: AppConfig): string { const ws = config.workspace.default.trim(); diff --git a/factory/packages/backend/src/db/actor-sqlite.ts b/factory/packages/backend/src/db/actor-sqlite.ts index fdae16f..76d688a 100644 --- a/factory/packages/backend/src/db/actor-sqlite.ts +++ b/factory/packages/backend/src/db/actor-sqlite.ts @@ -29,7 +29,7 @@ export interface ActorSqliteDbOptions> { /** * Override base directory for per-actor SQLite files. * - * Default: `/.openhandoff/backend/sqlite` + * Default: `/.sandbox-agent-factory/backend/sqlite` */ baseDir?: string; } @@ -53,7 +53,7 @@ export function actorSqliteDb>( }) as unknown as DatabaseProvider; } - const baseDir = options.baseDir ?? join(process.cwd(), ".openhandoff", "backend", "sqlite"); + const baseDir = options.baseDir ?? join(process.cwd(), ".sandbox-agent-factory", "backend", "sqlite"); const migrationsFolder = fileURLToPath(options.migrationsFolderUrl); return { diff --git a/factory/packages/backend/src/integrations/git/index.ts b/factory/packages/backend/src/integrations/git/index.ts index a27d469..a617d14 100644 --- a/factory/packages/backend/src/integrations/git/index.ts +++ b/factory/packages/backend/src/integrations/git/index.ts @@ -28,7 +28,7 @@ function ensureAskpassScript(): string { return cachedAskpassPath; } - const dir = mkdtempSync(resolve(tmpdir(), "openhandoff-git-askpass-")); + const dir = mkdtempSync(resolve(tmpdir(), "factory-git-askpass-")); const path = resolve(dir, "askpass.sh"); // Git invokes $GIT_ASKPASS with the prompt string as argv[1]. Provide both username and password. diff --git a/factory/packages/backend/src/integrations/sandbox-agent/client.ts b/factory/packages/backend/src/integrations/sandbox-agent/client.ts index 1dc28e7..27c5824 100644 --- a/factory/packages/backend/src/integrations/sandbox-agent/client.ts +++ b/factory/packages/backend/src/integrations/sandbox-agent/client.ts @@ -1,4 +1,4 @@ -import type { AgentType } from "@openhandoff/shared"; +import type { AgentType } from "@sandbox-agent/factory-shared"; import type { ListEventsRequest, ListPage, @@ -144,7 +144,7 @@ export class SandboxAgentClient { const modeId = modeIdForAgent(normalized.agent ?? this.agent); // Codex defaults to a restrictive "read-only" preset in some environments. - // For OpenHandoff automation we need to allow edits + command execution + network + // For Sandbox Agent Factory automation we need to allow edits + command execution + network // access (git push / PR creation). Use full-access where supported. // // If the agent doesn't support session modes, ignore. diff --git a/factory/packages/backend/src/providers/daytona/index.ts b/factory/packages/backend/src/providers/daytona/index.ts index 97d6aee..471552a 100644 --- a/factory/packages/backend/src/providers/daytona/index.ts +++ b/factory/packages/backend/src/providers/daytona/index.ts @@ -205,11 +205,11 @@ export class DaytonaProvider implements SandboxProvider { image: this.buildSnapshotImage(), envVars: this.buildEnvVars(), labels: { - "openhandoff.workspace": req.workspaceId, - "openhandoff.handoff": req.handoffId, - "openhandoff.repo_id": req.repoId, - "openhandoff.repo_remote": req.repoRemote, - "openhandoff.branch": req.branchName, + "factory.workspace": req.workspaceId, + "factory.handoff": req.handoffId, + "factory.repo_id": req.repoId, + "factory.repo_remote": req.repoRemote, + "factory.branch": req.branchName, }, autoStopInterval: this.config.autoStopInterval, }) @@ -220,7 +220,7 @@ export class DaytonaProvider implements SandboxProvider { state: sandbox.state ?? null }); - const repoDir = `/home/daytona/openhandoff/${req.workspaceId}/${req.repoId}/${req.handoffId}/repo`; + const repoDir = `/home/daytona/sandbox-agent-factory/${req.workspaceId}/${req.repoId}/${req.handoffId}/repo`; // Prepare a working directory for the agent. This must succeed for the handoff to work. const installStartedAt = Date.now(); @@ -258,8 +258,8 @@ export class DaytonaProvider implements SandboxProvider { `git fetch origin --prune`, // The handoff branch may not exist remotely yet (agent push creates it). Base off current branch (default branch). `if git show-ref --verify --quiet "refs/remotes/origin/${req.branchName}"; then git checkout -B "${req.branchName}" "origin/${req.branchName}"; else git checkout -B "${req.branchName}" "$(git branch --show-current 2>/dev/null || echo main)"; fi`, - `git config user.email "openhandoff@local" >/dev/null 2>&1 || true`, - `git config user.name "OpenHandoff" >/dev/null 2>&1 || true`, + `git config user.email "factory@local" >/dev/null 2>&1 || true`, + `git config user.name "Sandbox Agent Factory" >/dev/null 2>&1 || true`, ].join("; ") )}` ].join(" "), @@ -294,12 +294,12 @@ export class DaytonaProvider implements SandboxProvider { client.getSandbox(req.sandboxId) ); const labels = info.labels ?? {}; - const workspaceId = labels["openhandoff.workspace"] ?? req.workspaceId; - const repoId = labels["openhandoff.repo_id"] ?? ""; - const handoffId = labels["openhandoff.handoff"] ?? ""; + const workspaceId = labels["factory.workspace"] ?? req.workspaceId; + const repoId = labels["factory.repo_id"] ?? ""; + const handoffId = labels["factory.handoff"] ?? ""; const cwd = repoId && handoffId - ? `/home/daytona/openhandoff/${workspaceId}/${repoId}/${handoffId}/repo` + ? `/home/daytona/sandbox-agent-factory/${workspaceId}/${repoId}/${handoffId}/repo` : null; return { diff --git a/factory/packages/backend/src/providers/index.ts b/factory/packages/backend/src/providers/index.ts index 1410f94..ad0f725 100644 --- a/factory/packages/backend/src/providers/index.ts +++ b/factory/packages/backend/src/providers/index.ts @@ -1,5 +1,5 @@ -import type { ProviderId } from "@openhandoff/shared"; -import type { AppConfig } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import type { BackendDriver } from "../driver.js"; import { DaytonaProvider } from "./daytona/index.js"; import { LocalProvider } from "./local/index.js"; diff --git a/factory/packages/backend/src/providers/local/index.ts b/factory/packages/backend/src/providers/local/index.ts index 3317c24..c869f6a 100644 --- a/factory/packages/backend/src/providers/local/index.ts +++ b/factory/packages/backend/src/providers/local/index.ts @@ -77,7 +77,7 @@ export class LocalProvider implements SandboxProvider { private rootDir(): string { return expandHome( - this.config.rootDir?.trim() || "~/.local/share/openhandoff/local-sandboxes", + this.config.rootDir?.trim() || "~/.local/share/sandbox-agent-factory/local-sandboxes", ); } diff --git a/factory/packages/backend/src/providers/provider-api/index.ts b/factory/packages/backend/src/providers/provider-api/index.ts index 5735ec4..67a9af1 100644 --- a/factory/packages/backend/src/providers/provider-api/index.ts +++ b/factory/packages/backend/src/providers/provider-api/index.ts @@ -1,4 +1,4 @@ -import type { ProviderId } from "@openhandoff/shared"; +import type { ProviderId } from "@sandbox-agent/factory-shared"; export interface ProviderCapabilities { remote: boolean; diff --git a/factory/packages/backend/src/services/openhandoff-paths.ts b/factory/packages/backend/src/services/factory-paths.ts similarity index 65% rename from factory/packages/backend/src/services/openhandoff-paths.ts rename to factory/packages/backend/src/services/factory-paths.ts index 79ffca9..25d41ef 100644 --- a/factory/packages/backend/src/services/openhandoff-paths.ts +++ b/factory/packages/backend/src/services/factory-paths.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; @@ -9,17 +9,17 @@ function expandPath(input: string): string { return input; } -export function openhandoffDataDir(config: AppConfig): string { +export function factoryDataDir(config: AppConfig): string { // Keep data collocated with the backend DB by default. const dbPath = expandPath(config.backend.dbPath); return resolve(dirname(dbPath)); } -export function openhandoffRepoClonePath( +export function factoryRepoClonePath( config: AppConfig, workspaceId: string, repoId: string ): string { - return resolve(join(openhandoffDataDir(config), "repos", workspaceId, repoId)); + return resolve(join(factoryDataDir(config), "repos", workspaceId, repoId)); } diff --git a/factory/packages/backend/test/daytona-provider.test.ts b/factory/packages/backend/test/daytona-provider.test.ts index 27c9878..025f0f0 100644 --- a/factory/packages/backend/test/daytona-provider.test.ts +++ b/factory/packages/backend/test/daytona-provider.test.ts @@ -12,7 +12,7 @@ class RecordingDaytonaClient implements DaytonaClientLike { return { id: "sandbox-1", state: "started", - snapshot: "snapshot-openhandoff", + snapshot: "snapshot-factory", labels: {}, }; } @@ -21,7 +21,7 @@ class RecordingDaytonaClient implements DaytonaClientLike { return { id: sandboxId, state: "started", - snapshot: "snapshot-openhandoff", + snapshot: "snapshot-factory", labels: {}, }; } @@ -92,9 +92,9 @@ describe("daytona provider snapshot image behavior", () => { expect(commands).toContain("GIT_TERMINAL_PROMPT=0"); expect(commands).toContain("GIT_ASKPASS=/bin/echo"); - expect(handle.metadata.snapshot).toBe("snapshot-openhandoff"); + expect(handle.metadata.snapshot).toBe("snapshot-factory"); expect(handle.metadata.image).toBe("ubuntu:24.04"); - expect(handle.metadata.cwd).toBe("/home/daytona/openhandoff/default/repo-1/handoff-1/repo"); + expect(handle.metadata.cwd).toBe("/home/daytona/sandbox-agent-factory/default/repo-1/handoff-1/repo"); expect(client.executedCommands.length).toBeGreaterThan(0); }); diff --git a/factory/packages/backend/test/git-validate-remote.test.ts b/factory/packages/backend/test/git-validate-remote.test.ts index ea15ac7..4cea828 100644 --- a/factory/packages/backend/test/git-validate-remote.test.ts +++ b/factory/packages/backend/test/git-validate-remote.test.ts @@ -27,7 +27,7 @@ describe("validateRemote", () => { mkdirSync(brokenRepoDir, { recursive: true }); writeFileSync(resolve(brokenRepoDir, ".git"), "gitdir: /definitely/missing/worktree\n", "utf8"); await execFileAsync("git", ["init", remoteRepoDir]); - await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.name", "OpenHandoff Test"]); + await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.name", "Factory Test"]); await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.email", "test@example.com"]); writeFileSync(resolve(remoteRepoDir, "README.md"), "# test\n", "utf8"); await execFileAsync("git", ["-C", remoteRepoDir, "add", "README.md"]); diff --git a/factory/packages/backend/test/helpers/test-context.ts b/factory/packages/backend/test/helpers/test-context.ts index d779915..b163905 100644 --- a/factory/packages/backend/test/helpers/test-context.ts +++ b/factory/packages/backend/test/helpers/test-context.ts @@ -1,6 +1,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; import type { BackendDriver } from "../../src/driver.js"; import { initActorRuntimeContext } from "../../src/actors/context.js"; import { createProviderRegistry } from "../../src/providers/index.js"; diff --git a/factory/packages/backend/test/providers.test.ts b/factory/packages/backend/test/providers.test.ts index 6e3cfb2..a86f7d1 100644 --- a/factory/packages/backend/test/providers.test.ts +++ b/factory/packages/backend/test/providers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; import { createProviderRegistry } from "../src/providers/index.js"; function makeConfig(): AppConfig { @@ -10,7 +10,7 @@ function makeConfig(): AppConfig { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, diff --git a/factory/packages/backend/test/repo-normalize.test.ts b/factory/packages/backend/test/repo-normalize.test.ts index 593e26b..5314d95 100644 --- a/factory/packages/backend/test/repo-normalize.test.ts +++ b/factory/packages/backend/test/repo-normalize.test.ts @@ -3,41 +3,41 @@ import { normalizeRemoteUrl, repoIdFromRemote } from "../src/services/repo.js"; describe("normalizeRemoteUrl", () => { test("accepts GitHub shorthand owner/repo", () => { - expect(normalizeRemoteUrl("rivet-dev/openhandoff")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("rivet-dev/sandbox-agent-factory")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("accepts github.com/owner/repo without scheme", () => { - expect(normalizeRemoteUrl("github.com/rivet-dev/openhandoff")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("github.com/rivet-dev/sandbox-agent-factory")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("canonicalizes GitHub repo URLs without .git", () => { - expect(normalizeRemoteUrl("https://github.com/rivet-dev/openhandoff")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("https://github.com/rivet-dev/sandbox-agent-factory")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("canonicalizes GitHub non-clone URLs (e.g. /tree/main)", () => { - expect(normalizeRemoteUrl("https://github.com/rivet-dev/openhandoff/tree/main")).toBe( - "https://github.com/rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("https://github.com/rivet-dev/sandbox-agent-factory/tree/main")).toBe( + "https://github.com/rivet-dev/sandbox-agent-factory.git" ); }); test("does not rewrite scp-style ssh remotes", () => { - expect(normalizeRemoteUrl("git@github.com:rivet-dev/openhandoff.git")).toBe( - "git@github.com:rivet-dev/openhandoff.git" + expect(normalizeRemoteUrl("git@github.com:rivet-dev/sandbox-agent-factory.git")).toBe( + "git@github.com:rivet-dev/sandbox-agent-factory.git" ); }); }); describe("repoIdFromRemote", () => { test("repoId is stable across equivalent GitHub inputs", () => { - const a = repoIdFromRemote("rivet-dev/openhandoff"); - const b = repoIdFromRemote("https://github.com/rivet-dev/openhandoff.git"); - const c = repoIdFromRemote("https://github.com/rivet-dev/openhandoff/tree/main"); + const a = repoIdFromRemote("rivet-dev/sandbox-agent-factory"); + const b = repoIdFromRemote("https://github.com/rivet-dev/sandbox-agent-factory.git"); + const c = repoIdFromRemote("https://github.com/rivet-dev/sandbox-agent-factory/tree/main"); expect(a).toBe(b); expect(b).toBe(c); }); diff --git a/factory/packages/backend/test/workspace-isolation.test.ts b/factory/packages/backend/test/workspace-isolation.test.ts index ef31a40..bf3a22b 100644 --- a/factory/packages/backend/test/workspace-isolation.test.ts +++ b/factory/packages/backend/test/workspace-isolation.test.ts @@ -17,7 +17,7 @@ function createRepo(): { repoPath: string } { const repoPath = mkdtempSync(join(tmpdir(), "hf-isolation-repo-")); execFileSync("git", ["init"], { cwd: repoPath }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoPath }); - execFileSync("git", ["config", "user.name", "OpenHandoff Test"], { cwd: repoPath }); + execFileSync("git", ["config", "user.name", "Factory Test"], { cwd: repoPath }); writeFileSync(join(repoPath, "README.md"), "hello\n", "utf8"); execFileSync("git", ["add", "README.md"], { cwd: repoPath }); execFileSync("git", ["commit", "-m", "init"], { cwd: repoPath }); diff --git a/factory/packages/backend/tmp-decode-actors.mjs b/factory/packages/backend/tmp-decode-actors.mjs index a25790b..5f9c36c 100644 --- a/factory/packages/backend/tmp-decode-actors.mjs +++ b/factory/packages/backend/tmp-decode-actors.mjs @@ -20,7 +20,7 @@ function locationToNames(entry, names) { } for (const t of targets) { - const db = new Database(`/root/.local/share/openhandoff/rivetkit/databases/${t.actorId}.db`, { readonly: true }); + const db = new Database(`/root/.local/share/sandbox-agent-factory/rivetkit/databases/${t.actorId}.db`, { readonly: true }); const token = new TextDecoder().decode(db.query("SELECT value FROM kv WHERE hex(key)=?").get("03").value); await new Promise((resolve, reject) => { diff --git a/factory/packages/backend/tmp-dump-wfkeys.mjs b/factory/packages/backend/tmp-dump-wfkeys.mjs index 41df274..b38b6bc 100644 --- a/factory/packages/backend/tmp-dump-wfkeys.mjs +++ b/factory/packages/backend/tmp-dump-wfkeys.mjs @@ -1,6 +1,6 @@ import { Database } from "bun:sqlite"; -const db = new Database("/root/.local/share/openhandoff/rivetkit/databases/2e443238457137bf.db", { readonly: true }); +const db = new Database("/root/.local/share/sandbox-agent-factory/rivetkit/databases/2e443238457137bf.db", { readonly: true }); const rows = db.query("SELECT hex(key) as k, value as v FROM kv WHERE hex(key) LIKE ? ORDER BY key").all("07%"); const out = rows.map((r) => { const bytes = new Uint8Array(r.v); diff --git a/factory/packages/backend/tmp-inspect-deep.mjs b/factory/packages/backend/tmp-inspect-deep.mjs index fa5f8db..d2541b0 100644 --- a/factory/packages/backend/tmp-inspect-deep.mjs +++ b/factory/packages/backend/tmp-inspect-deep.mjs @@ -9,7 +9,7 @@ import { decodeReadRangeWire } from "/rivet-handoff-fixes/rivetkit-typescript/pa import { readRangeWireToOtlp } from "/rivet-handoff-fixes/rivetkit-typescript/packages/traces/src/read-range.ts"; const actorId = "2e443238457137bf"; -const db = new Database(`/root/.local/share/openhandoff/rivetkit/databases/${actorId}.db`, { readonly: true }); +const db = new Database(`/root/.local/share/sandbox-agent-factory/rivetkit/databases/${actorId}.db`, { readonly: true }); const row = db.query("SELECT value FROM kv WHERE hex(key)=?").get("03"); const token = new TextDecoder().decode(row.value); diff --git a/factory/packages/backend/tmp-inspect-stuck.mjs b/factory/packages/backend/tmp-inspect-stuck.mjs index 7bb8c08..e219436 100644 --- a/factory/packages/backend/tmp-inspect-stuck.mjs +++ b/factory/packages/backend/tmp-inspect-stuck.mjs @@ -14,7 +14,7 @@ function decodeAscii(u8) { } for (const actorId of actorIds) { - const dbPath = `/root/.local/share/openhandoff/rivetkit/databases/${actorId}.db`; + const dbPath = `/root/.local/share/sandbox-agent-factory/rivetkit/databases/${actorId}.db`; const db = new Database(dbPath, { readonly: true }); const wfStateRow = db.query("SELECT value FROM kv WHERE hex(key)=?").get("0715041501"); diff --git a/factory/packages/backend/tmp-inspect-workflow.mjs b/factory/packages/backend/tmp-inspect-workflow.mjs index 3bc9355..af76bf8 100644 --- a/factory/packages/backend/tmp-inspect-workflow.mjs +++ b/factory/packages/backend/tmp-inspect-workflow.mjs @@ -3,7 +3,7 @@ import { TO_CLIENT_VERSIONED, decodeWorkflowHistoryTransport } from "rivetkit/in import util from "node:util"; const actorId = "2e443238457137bf"; -const db = new Database(`/root/.local/share/openhandoff/rivetkit/databases/${actorId}.db`, { readonly: true }); +const db = new Database(`/root/.local/share/sandbox-agent-factory/rivetkit/databases/${actorId}.db`, { readonly: true }); const row = db.query("SELECT value FROM kv WHERE hex(key) = ?").get("03"); const token = new TextDecoder().decode(row.value); diff --git a/factory/packages/cli/package.json b/factory/packages/cli/package.json index e6ff8f4..87ef87c 100644 --- a/factory/packages/cli/package.json +++ b/factory/packages/cli/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/cli", + "name": "@sandbox-agent/factory-cli", "version": "0.1.0", "private": true, "type": "module", @@ -16,8 +16,8 @@ "dependencies": { "@iarna/toml": "^2.2.5", "@opentui/core": "^0.1.77", - "@openhandoff/client": "workspace:*", - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-client": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "zod": "^4.1.5" }, "devDependencies": { diff --git a/factory/packages/cli/src/backend/manager.ts b/factory/packages/cli/src/backend/manager.ts index 0ae01cb..0bd800b 100644 --- a/factory/packages/cli/src/backend/manager.ts +++ b/factory/packages/cli/src/backend/manager.ts @@ -11,8 +11,8 @@ import { import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { checkBackendHealth } from "@openhandoff/client"; -import type { AppConfig } from "@openhandoff/shared"; +import { checkBackendHealth } from "@sandbox-agent/factory-client"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import { CLI_BUILD_ID } from "../build-id.js"; const HEALTH_TIMEOUT_MS = 1_500; @@ -39,10 +39,10 @@ function backendStateDir(): string { const xdgDataHome = process.env.XDG_DATA_HOME?.trim(); if (xdgDataHome) { - return join(xdgDataHome, "openhandoff", "backend"); + return join(xdgDataHome, "sandbox-agent-factory", "backend"); } - return join(homedir(), ".local", "share", "openhandoff", "backend"); + return join(homedir(), ".local", "share", "sandbox-agent-factory", "backend"); } function backendPidPath(host: string, port: number): string { @@ -214,7 +214,7 @@ function resolveLaunchSpec(host: string, port: number): LaunchSpec { command: "pnpm", args: [ "--filter", - "@openhandoff/backend", + "@sandbox-agent/factory-backend", "exec", "bun", "src/index.ts", diff --git a/factory/packages/cli/src/index.ts b/factory/packages/cli/src/index.ts index 9d764ba..c4dbb62 100644 --- a/factory/packages/cli/src/index.ts +++ b/factory/packages/cli/src/index.ts @@ -2,14 +2,14 @@ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { homedir } from "node:os"; -import { AgentTypeSchema, CreateHandoffInputSchema, type HandoffRecord } from "@openhandoff/shared"; +import { AgentTypeSchema, CreateHandoffInputSchema, type HandoffRecord } from "@sandbox-agent/factory-shared"; import { readBackendMetadata, createBackendClientFromConfig, formatRelativeAge, groupHandoffStatus, summarizeHandoffs -} from "@openhandoff/client"; +} from "@sandbox-agent/factory-client"; import { ensureBackendRunning, getBackendStatus, diff --git a/factory/packages/cli/src/theme.ts b/factory/packages/cli/src/theme.ts index 5c6a917..32232aa 100644 --- a/factory/packages/cli/src/theme.ts +++ b/factory/packages/cli/src/theme.ts @@ -3,7 +3,7 @@ import { homedir } from "node:os"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { cwd } from "node:process"; import * as toml from "@iarna/toml"; -import type { AppConfig } from "@openhandoff/shared"; +import type { AppConfig } from "@sandbox-agent/factory-shared"; import opencodeThemePackJson from "./themes/opencode-pack.json" with { type: "json" }; export type ThemeMode = "dark" | "light"; @@ -101,7 +101,7 @@ export function resolveTuiTheme(config: AppConfig, baseDir = cwd()): TuiThemeRes return { theme: candidate.theme, name: candidate.name, - source: "openhandoff config", + source: "factory config", mode }; } diff --git a/factory/packages/cli/src/tui.ts b/factory/packages/cli/src/tui.ts index 458ede3..b08bb81 100644 --- a/factory/packages/cli/src/tui.ts +++ b/factory/packages/cli/src/tui.ts @@ -1,11 +1,11 @@ -import type { AppConfig, HandoffRecord } from "@openhandoff/shared"; +import type { AppConfig, HandoffRecord } from "@sandbox-agent/factory-shared"; import { spawnSync } from "node:child_process"; import { createBackendClientFromConfig, filterHandoffs, formatRelativeAge, groupHandoffStatus -} from "@openhandoff/client"; +} from "@sandbox-agent/factory-client"; import { CLI_BUILD_ID } from "./build-id.js"; import { resolveTuiTheme, type TuiTheme } from "./theme.js"; @@ -338,7 +338,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise { }); import { ensureBackendRunning, parseBackendPort } from "../src/backend/manager.js"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; function backendStateFile(baseDir: string, host: string, port: number, suffix: string): string { const sanitized = host @@ -62,7 +62,7 @@ describe("backend manager", () => { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, diff --git a/factory/packages/cli/test/theme.test.ts b/factory/packages/cli/test/theme.test.ts index 6ccd902..608426d 100644 --- a/factory/packages/cli/test/theme.test.ts +++ b/factory/packages/cli/test/theme.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { ConfigSchema, type AppConfig } from "@openhandoff/shared"; +import { ConfigSchema, type AppConfig } from "@sandbox-agent/factory-shared"; import { resolveTuiTheme } from "../src/theme.js"; function withEnv(key: string, value: string | undefined): void { @@ -25,7 +25,7 @@ describe("resolveTuiTheme", () => { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, @@ -98,7 +98,7 @@ describe("resolveTuiTheme", () => { expect(resolution.theme.background).toBe("#0a0a0a"); }); - it("prefers explicit openhandoff theme override from config", () => { + it("prefers explicit factory theme override from config", () => { tempDir = mkdtempSync(join(tmpdir(), "hf-theme-test-")); withEnv("XDG_STATE_HOME", join(tempDir, "state")); withEnv("XDG_CONFIG_HOME", join(tempDir, "config")); @@ -107,6 +107,6 @@ describe("resolveTuiTheme", () => { const resolution = resolveTuiTheme(config, tempDir); expect(resolution.name).toBe("opencode-default"); - expect(resolution.source).toBe("openhandoff config"); + expect(resolution.source).toBe("factory config"); }); }); diff --git a/factory/packages/cli/test/tui-format.test.ts b/factory/packages/cli/test/tui-format.test.ts index e7821f9..79020e6 100644 --- a/factory/packages/cli/test/tui-format.test.ts +++ b/factory/packages/cli/test/tui-format.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { HandoffRecord } from "@openhandoff/shared"; -import { filterHandoffs, fuzzyMatch } from "@openhandoff/client"; +import type { HandoffRecord } from "@sandbox-agent/factory-shared"; +import { filterHandoffs, fuzzyMatch } from "@sandbox-agent/factory-client"; import { formatRows } from "../src/tui.js"; const sample: HandoffRecord = { diff --git a/factory/packages/cli/test/workspace-config.test.ts b/factory/packages/cli/test/workspace-config.test.ts index 0666984..86cdc42 100644 --- a/factory/packages/cli/test/workspace-config.test.ts +++ b/factory/packages/cli/test/workspace-config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ConfigSchema } from "@openhandoff/shared"; +import { ConfigSchema } from "@sandbox-agent/factory-shared"; import { resolveWorkspace } from "../src/workspace/config.js"; describe("cli workspace resolution", () => { @@ -11,7 +11,7 @@ describe("cli workspace resolution", () => { backend: { host: "127.0.0.1", port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", + dbPath: "~/.local/share/sandbox-agent-factory/handoff.db", opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, diff --git a/factory/packages/client/package.json b/factory/packages/client/package.json index ebb82f8..93374e4 100644 --- a/factory/packages/client/package.json +++ b/factory/packages/client/package.json @@ -1,12 +1,43 @@ { - "name": "@openhandoff/client", + "name": "@sandbox-agent/factory-client", "version": "0.1.0", "private": true, "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./backend": { + "types": "./dist/backend.d.ts", + "import": "./dist/backend.js" + }, + "./workbench": { + "types": "./dist/workbench.d.ts", + "import": "./dist/workbench.js" + }, + "./view-model": { + "types": "./dist/view-model.d.ts", + "import": "./dist/view-model.js" + } + }, + "typesVersions": { + "*": { + "backend": [ + "dist/backend.d.ts" + ], + "view-model": [ + "dist/view-model.d.ts" + ], + "workbench": [ + "dist/workbench.d.ts" + ] + } + }, "scripts": { - "build": "tsup src/index.ts --format esm --dts", + "build": "tsup src/index.ts src/backend.ts src/workbench.ts src/view-model.ts --format esm --dts", "typecheck": "tsc --noEmit", "test": "vitest run", "test:e2e:full": "HF_ENABLE_DAEMON_FULL_E2E=1 vitest run test/e2e/full-integration-e2e.test.ts", @@ -14,7 +45,7 @@ "test:e2e:workbench-load": "HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E=1 vitest run test/e2e/workbench-load-e2e.test.ts" }, "dependencies": { - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "rivetkit": "link:../../../../../handoff/rivet-checkout/rivetkit-typescript/packages/rivetkit" }, "devDependencies": { diff --git a/factory/packages/client/src/backend-client.ts b/factory/packages/client/src/backend-client.ts index 86288fb..e071546 100644 --- a/factory/packages/client/src/backend-client.ts +++ b/factory/packages/client/src/backend-client.ts @@ -26,7 +26,7 @@ import type { RepoStackActionResult, RepoRecord, SwitchResult -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { sandboxInstanceKey, workspaceKey } from "./keys.js"; export type HandoffAction = "push" | "sync" | "merge" | "archive" | "kill"; diff --git a/factory/packages/client/src/backend.ts b/factory/packages/client/src/backend.ts new file mode 100644 index 0000000..238a032 --- /dev/null +++ b/factory/packages/client/src/backend.ts @@ -0,0 +1 @@ +export * from "./backend-client.js"; diff --git a/factory/packages/client/src/mock/workbench-client.ts b/factory/packages/client/src/mock/workbench-client.ts index b2738dd..1ee27f7 100644 --- a/factory/packages/client/src/mock/workbench-client.ts +++ b/factory/packages/client/src/mock/workbench-client.ts @@ -26,7 +26,7 @@ import type { WorkbenchAgentTab as AgentTab, WorkbenchHandoff as Handoff, WorkbenchTranscriptEvent as TranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import type { HandoffWorkbenchClient } from "../workbench-client.js"; function buildTranscriptEvent(params: { @@ -48,10 +48,14 @@ function buildTranscriptEvent(params: { } class MockWorkbenchStore implements HandoffWorkbenchClient { - private snapshot = buildInitialMockLayoutViewModel(); + private snapshot: HandoffWorkbenchSnapshot; private listeners = new Set<() => void>(); private pendingTimers = new Map>(); + constructor(workspaceId: string) { + this.snapshot = buildInitialMockLayoutViewModel(workspaceId); + } + getSnapshot(): HandoffWorkbenchSnapshot { return this.snapshot; } @@ -103,6 +107,17 @@ class MockWorkbenchStore implements HandoffWorkbenchClient { ...current, handoffs: [nextHandoff, ...current.handoffs], })); + + const task = input.task.trim(); + if (task) { + await this.sendMessage({ + handoffId: id, + tabId, + text: task, + attachments: [], + }); + } + return { handoffId: id, tabId }; } @@ -149,6 +164,13 @@ class MockWorkbenchStore implements HandoffWorkbenchClient { })); } + async pushHandoff(input: HandoffWorkbenchSelectInput): Promise { + this.updateHandoff(input.handoffId, (handoff) => ({ + ...handoff, + updatedAtMs: nowMs(), + })); + } + async revertFile(input: HandoffWorkbenchDiffInput): Promise { this.updateHandoff(input.handoffId, (handoff) => { const file = handoff.fileChanges.find((entry) => entry.path === input.path); @@ -195,8 +217,11 @@ class MockWorkbenchStore implements HandoffWorkbenchClient { this.updateHandoff(input.handoffId, (currentHandoff) => { const isFirstOnHandoff = currentHandoff.status === "new"; - const newTitle = isFirstOnHandoff ? (text.length > 50 ? `${text.slice(0, 47)}...` : text) : currentHandoff.title; - const newBranch = isFirstOnHandoff ? `feat/${slugify(newTitle)}` : currentHandoff.branch; + const synthesizedTitle = text.length > 50 ? `${text.slice(0, 47)}...` : text; + const newTitle = + isFirstOnHandoff && currentHandoff.title === "New Handoff" ? synthesizedTitle : currentHandoff.title; + const newBranch = + isFirstOnHandoff && !currentHandoff.branch ? `feat/${slugify(synthesizedTitle)}` : currentHandoff.branch; const userMessageLines = [text, ...input.attachments.map((attachment) => `@ ${attachment.filePath}:${attachment.lineNumber}`)]; const userEvent = buildTranscriptEvent({ sessionId: input.tabId, @@ -435,11 +460,13 @@ function candidateEventIndex(handoff: Handoff, tabId: string): number { return (tab?.transcript.length ?? 0) + 1; } -let sharedMockWorkbenchClient: HandoffWorkbenchClient | null = null; +const mockWorkbenchClients = new Map(); -export function getSharedMockWorkbenchClient(): HandoffWorkbenchClient { - if (!sharedMockWorkbenchClient) { - sharedMockWorkbenchClient = new MockWorkbenchStore(); +export function getMockWorkbenchClient(workspaceId = "default"): HandoffWorkbenchClient { + let client = mockWorkbenchClients.get(workspaceId); + if (!client) { + client = new MockWorkbenchStore(workspaceId); + mockWorkbenchClients.set(workspaceId, client); } - return sharedMockWorkbenchClient; + return client; } diff --git a/factory/packages/client/src/remote/workbench-client.ts b/factory/packages/client/src/remote/workbench-client.ts index 720613b..af022f4 100644 --- a/factory/packages/client/src/remote/workbench-client.ts +++ b/factory/packages/client/src/remote/workbench-client.ts @@ -12,7 +12,7 @@ import type { HandoffWorkbenchSnapshot, HandoffWorkbenchTabInput, HandoffWorkbenchUpdateDraftInput, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import type { BackendClient } from "../backend-client.js"; import { groupWorkbenchProjects } from "../workbench-model.js"; import type { HandoffWorkbenchClient } from "../workbench-client.js"; @@ -93,6 +93,11 @@ class RemoteWorkbenchStore implements HandoffWorkbenchClient { await this.refresh(); } + async pushHandoff(input: HandoffWorkbenchSelectInput): Promise { + await this.backend.runAction(this.workspaceId, input.handoffId, "push"); + await this.refresh(); + } + async revertFile(input: HandoffWorkbenchDiffInput): Promise { await this.backend.revertWorkbenchFile(this.workspaceId, input); await this.refresh(); diff --git a/factory/packages/client/src/view-model.ts b/factory/packages/client/src/view-model.ts index 344f8a5..99ce33a 100644 --- a/factory/packages/client/src/view-model.ts +++ b/factory/packages/client/src/view-model.ts @@ -1,4 +1,4 @@ -import type { HandoffRecord, HandoffStatus } from "@openhandoff/shared"; +import type { HandoffRecord, HandoffStatus } from "@sandbox-agent/factory-shared"; export const HANDOFF_STATUS_GROUPS = [ "queued", diff --git a/factory/packages/client/src/workbench-client.ts b/factory/packages/client/src/workbench-client.ts index 1738c19..7f26dbf 100644 --- a/factory/packages/client/src/workbench-client.ts +++ b/factory/packages/client/src/workbench-client.ts @@ -12,9 +12,9 @@ import type { HandoffWorkbenchSnapshot, HandoffWorkbenchTabInput, HandoffWorkbenchUpdateDraftInput, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import type { BackendClient } from "./backend-client.js"; -import { getSharedMockWorkbenchClient } from "./mock/workbench-client.js"; +import { getMockWorkbenchClient } from "./mock/workbench-client.js"; import { createRemoteWorkbenchClient } from "./remote/workbench-client.js"; export type HandoffWorkbenchClientMode = "mock" | "remote"; @@ -34,6 +34,7 @@ export interface HandoffWorkbenchClient { renameBranch(input: HandoffWorkbenchRenameInput): Promise; archiveHandoff(input: HandoffWorkbenchSelectInput): Promise; publishPr(input: HandoffWorkbenchSelectInput): Promise; + pushHandoff(input: HandoffWorkbenchSelectInput): Promise; revertFile(input: HandoffWorkbenchDiffInput): Promise; updateDraft(input: HandoffWorkbenchUpdateDraftInput): Promise; sendMessage(input: HandoffWorkbenchSendMessageInput): Promise; @@ -49,7 +50,7 @@ export function createHandoffWorkbenchClient( options: CreateHandoffWorkbenchClientOptions, ): HandoffWorkbenchClient { if (options.mode === "mock") { - return getSharedMockWorkbenchClient(); + return getMockWorkbenchClient(options.workspaceId); } if (!options.backend) { diff --git a/factory/packages/client/src/workbench-model.ts b/factory/packages/client/src/workbench-model.ts index 51dd4f5..7e7a896 100644 --- a/factory/packages/client/src/workbench-model.ts +++ b/factory/packages/client/src/workbench-model.ts @@ -12,7 +12,7 @@ import type { WorkbenchProjectSection, WorkbenchRepo, WorkbenchTranscriptEvent as TranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; export const MODEL_GROUPS: ModelGroup[] = [ { @@ -913,7 +913,7 @@ export function buildInitialHandoffs(): Handoff[] { ]; } -export function buildInitialMockLayoutViewModel(): HandoffWorkbenchSnapshot { +export function buildInitialMockLayoutViewModel(workspaceId = "default"): HandoffWorkbenchSnapshot { const repos: WorkbenchRepo[] = [ { id: "acme-backend", label: "acme/backend" }, { id: "acme-frontend", label: "acme/frontend" }, @@ -921,7 +921,7 @@ export function buildInitialMockLayoutViewModel(): HandoffWorkbenchSnapshot { ]; const handoffs = buildInitialHandoffs(); return { - workspaceId: "default", + workspaceId, repos, projects: groupWorkbenchProjects(repos, handoffs), handoffs, @@ -960,6 +960,5 @@ export function groupWorkbenchProjects(repos: WorkbenchRepo[], handoffs: Handoff updatedAtMs: project.handoffs.length > 0 ? Math.max(...project.handoffs.map((handoff) => handoff.updatedAtMs)) : project.updatedAtMs, })) - .filter((project) => project.handoffs.length > 0) .sort((a, b) => b.updatedAtMs - a.updatedAtMs); } diff --git a/factory/packages/client/src/workbench.ts b/factory/packages/client/src/workbench.ts new file mode 100644 index 0000000..162025d --- /dev/null +++ b/factory/packages/client/src/workbench.ts @@ -0,0 +1 @@ +export * from "./workbench-client.js"; diff --git a/factory/packages/client/test/e2e/full-integration-e2e.test.ts b/factory/packages/client/test/e2e/full-integration-e2e.test.ts index 74f29d4..3f5e52a 100644 --- a/factory/packages/client/test/e2e/full-integration-e2e.test.ts +++ b/factory/packages/client/test/e2e/full-integration-e2e.test.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { describe, expect, it } from "vitest"; -import type { HistoryEvent, RepoOverview } from "@openhandoff/shared"; +import type { HistoryEvent, RepoOverview } from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_FULL_E2E = process.env.HF_ENABLE_DAEMON_FULL_E2E === "1"; diff --git a/factory/packages/client/test/e2e/github-pr-e2e.test.ts b/factory/packages/client/test/e2e/github-pr-e2e.test.ts index bd489fa..e3cbe8e 100644 --- a/factory/packages/client/test/e2e/github-pr-e2e.test.ts +++ b/factory/packages/client/test/e2e/github-pr-e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { HandoffRecord, HistoryEvent } from "@openhandoff/shared"; +import type { HandoffRecord, HistoryEvent } from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_E2E = process.env.HF_ENABLE_DAEMON_E2E === "1"; diff --git a/factory/packages/client/test/e2e/workbench-e2e.test.ts b/factory/packages/client/test/e2e/workbench-e2e.test.ts index 00e8167..3807955 100644 --- a/factory/packages/client/test/e2e/workbench-e2e.test.ts +++ b/factory/packages/client/test/e2e/workbench-e2e.test.ts @@ -1,13 +1,15 @@ import { execFile } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import type { + HandoffRecord, HandoffWorkbenchSnapshot, WorkbenchAgentTab, WorkbenchHandoff, WorkbenchModelId, WorkbenchTranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_WORKBENCH_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_E2E === "1"; @@ -21,6 +23,10 @@ function requiredEnv(name: string): string { return value; } +function requiredRepoRemote(): string { + return process.env.HF_E2E_REPO_REMOTE?.trim() || requiredEnv("HF_E2E_GITHUB_REPO"); +} + function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId { const value = process.env[name]?.trim(); switch (value) { @@ -38,14 +44,66 @@ async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)); } -async function seedSandboxFile(workspaceId: string, handoffId: string, filePath: string, content: string): Promise { - const repoPath = `/root/.local/share/openhandoff/local-sandboxes/${workspaceId}/${handoffId}/repo`; +function backendPortFromEndpoint(endpoint: string): string { + const url = new URL(endpoint); + if (url.port) { + return url.port; + } + return url.protocol === "https:" ? "443" : "80"; +} + +async function resolveBackendContainerName(endpoint: string): Promise { + const explicit = process.env.HF_E2E_BACKEND_CONTAINER?.trim(); + if (explicit) { + if (explicit.toLowerCase() === "host") { + return null; + } + return explicit; + } + + const { stdout } = await execFileAsync("docker", [ + "ps", + "--filter", + `publish=${backendPortFromEndpoint(endpoint)}`, + "--format", + "{{.Names}}", + ]); + const containerName = stdout + .split("\n") + .map((line) => line.trim()) + .find(Boolean); + + return containerName ?? null; +} + +function sandboxRepoPath(record: HandoffRecord): string { + const activeSandbox = + record.sandboxes.find((sandbox) => sandbox.sandboxId === record.activeSandboxId) ?? + record.sandboxes.find((sandbox) => typeof sandbox.cwd === "string" && sandbox.cwd.length > 0); + const cwd = activeSandbox?.cwd?.trim(); + if (!cwd) { + throw new Error(`No sandbox cwd is available for handoff ${record.handoffId}`); + } + return cwd; +} + +async function seedSandboxFile(endpoint: string, record: HandoffRecord, filePath: string, content: string): Promise { + const repoPath = sandboxRepoPath(record); + const containerName = await resolveBackendContainerName(endpoint); + if (!containerName) { + const directory = + filePath.includes("/") ? `${repoPath}/${filePath.slice(0, filePath.lastIndexOf("/"))}` : repoPath; + await mkdir(directory, { recursive: true }); + await writeFile(`${repoPath}/${filePath}`, `${content}\n`, "utf8"); + return; + } + const script = [ `cd ${JSON.stringify(repoPath)}`, `mkdir -p ${JSON.stringify(filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : ".")}`, `printf '%s\\n' ${JSON.stringify(content)} > ${JSON.stringify(filePath)}`, ].join(" && "); - await execFileAsync("docker", ["exec", "openhandoff-backend-1", "bash", "-lc", script]); + await execFileAsync("docker", ["exec", containerName, "bash", "-lc", script]); } async function poll( @@ -166,7 +224,7 @@ describe("e2e(client): workbench flows", () => { const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet"; const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default"; - const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO"); + const repoRemote = requiredRepoRemote(); const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-4o"); const runId = `wb-${Date.now().toString(36)}`; const expectedFile = `${runId}.txt`; @@ -215,7 +273,8 @@ describe("e2e(client): workbench flows", () => { expect(findTab(initialCompleted, primaryTab.id).sessionId).toBeTruthy(); expect(transcriptIncludesAgentText(findTab(initialCompleted, primaryTab.id).transcript, expectedInitialReply)).toBe(true); - await seedSandboxFile(workspaceId, created.handoffId, expectedFile, runId); + const detail = await client.getHandoff(workspaceId, created.handoffId); + await seedSandboxFile(endpoint, detail, expectedFile, runId); const fileSeeded = await poll( "seeded sandbox file reflected in workbench", diff --git a/factory/packages/client/test/e2e/workbench-load-e2e.test.ts b/factory/packages/client/test/e2e/workbench-load-e2e.test.ts index 230ae49..c1a01f6 100644 --- a/factory/packages/client/test/e2e/workbench-load-e2e.test.ts +++ b/factory/packages/client/test/e2e/workbench-load-e2e.test.ts @@ -5,7 +5,7 @@ import type { WorkbenchHandoff, WorkbenchModelId, WorkbenchTranscriptEvent, -} from "@openhandoff/shared"; +} from "@sandbox-agent/factory-shared"; import { createBackendClient } from "../../src/backend-client.js"; const RUN_WORKBENCH_LOAD_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E === "1"; @@ -18,6 +18,10 @@ function requiredEnv(name: string): string { return value; } +function requiredRepoRemote(): string { + return process.env.HF_E2E_REPO_REMOTE?.trim() || requiredEnv("HF_E2E_GITHUB_REPO"); +} + function workbenchModelEnv(name: string, fallback: WorkbenchModelId): WorkbenchModelId { const value = process.env[name]?.trim(); switch (value) { @@ -196,7 +200,7 @@ describe("e2e(client): workbench load", () => { async () => { const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet"; const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default"; - const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO"); + const repoRemote = requiredRepoRemote(); const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-4o"); const handoffCount = intEnv("HF_LOAD_HANDOFF_COUNT", 3); const extraSessionCount = intEnv("HF_LOAD_EXTRA_SESSION_COUNT", 2); diff --git a/factory/packages/client/test/view-model.test.ts b/factory/packages/client/test/view-model.test.ts index 823ab7d..fac0ac0 100644 --- a/factory/packages/client/test/view-model.test.ts +++ b/factory/packages/client/test/view-model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { HandoffRecord } from "@openhandoff/shared"; +import type { HandoffRecord } from "@sandbox-agent/factory-shared"; import { filterHandoffs, formatRelativeAge, diff --git a/factory/packages/client/test/workbench-client.test.ts b/factory/packages/client/test/workbench-client.test.ts new file mode 100644 index 0000000..79d7bd0 --- /dev/null +++ b/factory/packages/client/test/workbench-client.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from "vitest"; +import type { BackendClient } from "../src/backend-client.js"; +import { createHandoffWorkbenchClient } from "../src/workbench-client.js"; + +async function sleep(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe("createHandoffWorkbenchClient", () => { + it("scopes mock clients by workspace", async () => { + const alpha = createHandoffWorkbenchClient({ + mode: "mock", + workspaceId: "mock-alpha", + }); + const beta = createHandoffWorkbenchClient({ + mode: "mock", + workspaceId: "mock-beta", + }); + + const alphaInitial = alpha.getSnapshot(); + const betaInitial = beta.getSnapshot(); + expect(alphaInitial.workspaceId).toBe("mock-alpha"); + expect(betaInitial.workspaceId).toBe("mock-beta"); + + await alpha.createHandoff({ + repoId: alphaInitial.repos[0]!.id, + task: "Ship alpha-only change", + title: "Alpha only", + }); + + expect(alpha.getSnapshot().handoffs).toHaveLength(alphaInitial.handoffs.length + 1); + expect(beta.getSnapshot().handoffs).toHaveLength(betaInitial.handoffs.length); + }); + + it("uses the initial task to bootstrap a new mock handoff session", async () => { + const client = createHandoffWorkbenchClient({ + mode: "mock", + workspaceId: "mock-onboarding", + }); + const snapshot = client.getSnapshot(); + + const created = await client.createHandoff({ + repoId: snapshot.repos[0]!.id, + task: "Reply with exactly: MOCK_WORKBENCH_READY", + title: "Mock onboarding", + branch: "feat/mock-onboarding", + model: "gpt-4o", + }); + + const runningHandoff = client.getSnapshot().handoffs.find((handoff) => handoff.id === created.handoffId); + expect(runningHandoff).toEqual( + expect.objectContaining({ + title: "Mock onboarding", + branch: "feat/mock-onboarding", + status: "running", + }), + ); + expect(runningHandoff?.tabs[0]).toEqual( + expect.objectContaining({ + id: created.tabId, + created: true, + status: "running", + }), + ); + expect(runningHandoff?.tabs[0]?.transcript).toEqual([ + expect.objectContaining({ + sender: "client", + payload: expect.objectContaining({ + method: "session/prompt", + }), + }), + ]); + + await sleep(2_700); + + const completedHandoff = client.getSnapshot().handoffs.find((handoff) => handoff.id === created.handoffId); + expect(completedHandoff?.status).toBe("idle"); + expect(completedHandoff?.tabs[0]).toEqual( + expect.objectContaining({ + status: "idle", + unread: true, + }), + ); + expect(completedHandoff?.tabs[0]?.transcript).toEqual([ + expect.objectContaining({ sender: "client" }), + expect.objectContaining({ sender: "agent" }), + ]); + }); + + it("routes remote push actions through the backend boundary", async () => { + const actions: Array<{ workspaceId: string; handoffId: string; action: string }> = []; + let snapshotReads = 0; + const backend = { + async runAction(workspaceId: string, handoffId: string, action: string): Promise { + actions.push({ workspaceId, handoffId, action }); + }, + async getWorkbench(workspaceId: string) { + snapshotReads += 1; + return { + workspaceId, + repos: [], + projects: [], + handoffs: [], + }; + }, + subscribeWorkbench(): () => void { + return () => {}; + }, + } as unknown as BackendClient; + + const client = createHandoffWorkbenchClient({ + mode: "remote", + backend, + workspaceId: "remote-ws", + }); + + await client.pushHandoff({ handoffId: "handoff-123" }); + + expect(actions).toEqual([ + { + workspaceId: "remote-ws", + handoffId: "handoff-123", + action: "push", + }, + ]); + expect(snapshotReads).toBe(1); + }); +}); diff --git a/factory/packages/frontend-errors/package.json b/factory/packages/frontend-errors/package.json index c30f796..77caa47 100644 --- a/factory/packages/frontend-errors/package.json +++ b/factory/packages/frontend-errors/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/frontend-errors", + "name": "@sandbox-agent/factory-frontend-errors", "version": "0.1.0", "private": true, "type": "module", diff --git a/factory/packages/frontend-errors/src/client.ts b/factory/packages/frontend-errors/src/client.ts index f055704..9981d80 100644 --- a/factory/packages/frontend-errors/src/client.ts +++ b/factory/packages/frontend-errors/src/client.ts @@ -6,8 +6,8 @@ interface FrontendErrorCollectorGlobal { declare global { interface Window { - __OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__?: FrontendErrorCollectorGlobal; - __OPENHANDOFF_FRONTEND_ERROR_CONTEXT__?: FrontendErrorContext; + __FACTORY_FRONTEND_ERROR_COLLECTOR__?: FrontendErrorCollectorGlobal; + __FACTORY_FRONTEND_ERROR_CONTEXT__?: FrontendErrorContext; } } @@ -17,11 +17,11 @@ export function setFrontendErrorContext(context: FrontendErrorContext): void { } const nextContext = sanitizeContext(context); - window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ = { - ...(window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ ?? {}), + window.__FACTORY_FRONTEND_ERROR_CONTEXT__ = { + ...(window.__FACTORY_FRONTEND_ERROR_CONTEXT__ ?? {}), ...nextContext, }; - window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__?.setContext(nextContext); + window.__FACTORY_FRONTEND_ERROR_COLLECTOR__?.setContext(nextContext); } function sanitizeContext(input: FrontendErrorContext): FrontendErrorContext { diff --git a/factory/packages/frontend-errors/src/router.ts b/factory/packages/frontend-errors/src/router.ts index aa0bbe7..f3b9d4d 100644 --- a/factory/packages/frontend-errors/src/router.ts +++ b/factory/packages/frontend-errors/src/router.ts @@ -4,8 +4,8 @@ import { dirname, join, resolve } from "node:path"; import { Hono } from "hono"; import type { FrontendErrorContext, FrontendErrorKind, FrontendErrorLogEvent } from "./types.js"; -const DEFAULT_RELATIVE_LOG_PATH = ".openhandoff/logs/frontend-errors.ndjson"; -const DEFAULT_REPORTER = "openhandoff-frontend"; +const DEFAULT_RELATIVE_LOG_PATH = ".sandbox-agent-factory/logs/frontend-errors.ndjson"; +const DEFAULT_REPORTER = "sandbox-agent-factory"; const MAX_FIELD_LENGTH = 12_000; export interface FrontendErrorCollectorRouterOptions { diff --git a/factory/packages/frontend-errors/src/script.ts b/factory/packages/frontend-errors/src/script.ts index 66101bf..025cc39 100644 --- a/factory/packages/frontend-errors/src/script.ts +++ b/factory/packages/frontend-errors/src/script.ts @@ -1,6 +1,6 @@ import type { FrontendErrorCollectorScriptOptions } from "./types.js"; -const DEFAULT_REPORTER = "openhandoff-frontend"; +const DEFAULT_REPORTER = "sandbox-agent-factory"; export function createFrontendErrorCollectorScript( options: FrontendErrorCollectorScriptOptions @@ -17,13 +17,13 @@ export function createFrontendErrorCollectorScript( return; } - if (window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__) { + if (window.__FACTORY_FRONTEND_ERROR_COLLECTOR__) { return; } var config = ${JSON.stringify(config)}; - var sharedContext = window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ || {}; - window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ = sharedContext; + var sharedContext = window.__FACTORY_FRONTEND_ERROR_CONTEXT__ || {}; + window.__FACTORY_FRONTEND_ERROR_CONTEXT__ = sharedContext; function now() { return Date.now(); @@ -124,7 +124,7 @@ export function createFrontendErrorCollectorScript( }); } - window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__ = { + window.__FACTORY_FRONTEND_ERROR_COLLECTOR__ = { setContext: function (nextContext) { if (!nextContext || typeof nextContext !== "object") { return; diff --git a/factory/packages/frontend-errors/src/vite.ts b/factory/packages/frontend-errors/src/vite.ts index f52eccb..312d65c 100644 --- a/factory/packages/frontend-errors/src/vite.ts +++ b/factory/packages/frontend-errors/src/vite.ts @@ -4,7 +4,7 @@ import type { Plugin } from "vite"; import { createFrontendErrorCollectorRouter, defaultFrontendErrorLogPath } from "./router.js"; import { createFrontendErrorCollectorScript } from "./script.js"; -const DEFAULT_MOUNT_PATH = "/__openhandoff/frontend-errors"; +const DEFAULT_MOUNT_PATH = "/__factory/frontend-errors"; const DEFAULT_EVENT_PATH = "/events"; export interface FrontendErrorCollectorVitePluginOptions { @@ -20,7 +20,7 @@ export function frontendErrorCollectorVitePlugin( ): Plugin { const mountPath = normalizePath(options.mountPath ?? DEFAULT_MOUNT_PATH); const logFilePath = options.logFilePath ?? defaultFrontendErrorLogPath(process.cwd()); - const reporter = options.reporter ?? "openhandoff-vite"; + const reporter = options.reporter ?? "factory-vite"; const endpoint = `${mountPath}${DEFAULT_EVENT_PATH}`; const router = createFrontendErrorCollectorRouter({ @@ -31,7 +31,7 @@ export function frontendErrorCollectorVitePlugin( const listener = getRequestListener(mountApp.fetch); return { - name: "openhandoff:frontend-error-collector", + name: "factory:frontend-error-collector", apply: "serve", transformIndexHtml(html) { return { diff --git a/factory/packages/frontend-errors/test/router.test.ts b/factory/packages/frontend-errors/test/router.test.ts index bed1d13..235a585 100644 --- a/factory/packages/frontend-errors/test/router.test.ts +++ b/factory/packages/frontend-errors/test/router.test.ts @@ -47,9 +47,9 @@ describe("frontend error collector router", () => { describe("frontend error collector script", () => { test("embeds configured endpoint", () => { const script = createFrontendErrorCollectorScript({ - endpoint: "/__openhandoff/frontend-errors/events", + endpoint: "/__factory/frontend-errors/events", }); - expect(script).toContain("/__openhandoff/frontend-errors/events"); + expect(script).toContain("/__factory/frontend-errors/events"); expect(script).toContain("window.addEventListener(\"error\""); }); }); diff --git a/factory/packages/frontend/index.html b/factory/packages/frontend/index.html index f4d55ce..6506468 100644 --- a/factory/packages/frontend/index.html +++ b/factory/packages/frontend/index.html @@ -10,7 +10,7 @@ - OpenHandoff + Sandbox Agent Factory
diff --git a/factory/packages/frontend/package.json b/factory/packages/frontend/package.json index 4d00b9f..09c1871 100644 --- a/factory/packages/frontend/package.json +++ b/factory/packages/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "@openhandoff/frontend", + "name": "@sandbox-agent/factory-frontend", "version": "0.1.0", "private": true, "type": "module", @@ -10,9 +10,9 @@ "test": "vitest run" }, "dependencies": { - "@openhandoff/client": "workspace:*", - "@openhandoff/frontend-errors": "workspace:*", - "@openhandoff/shared": "workspace:*", + "@sandbox-agent/factory-client": "workspace:*", + "@sandbox-agent/factory-frontend-errors": "workspace:*", + "@sandbox-agent/factory-shared": "workspace:*", "@tanstack/react-query": "^5.85.5", "@tanstack/react-router": "^1.132.23", "baseui": "^16.1.1", diff --git a/factory/packages/frontend/src/app/router.tsx b/factory/packages/frontend/src/app/router.tsx index 2719736..84faa8d 100644 --- a/factory/packages/frontend/src/app/router.tsx +++ b/factory/packages/frontend/src/app/router.tsx @@ -1,5 +1,5 @@ -import { useEffect } from "react"; -import { setFrontendErrorContext } from "@openhandoff/frontend-errors/client"; +import { useEffect, useSyncExternalStore } from "react"; +import { setFrontendErrorContext } from "@sandbox-agent/factory-frontend-errors/client"; import { Navigate, Outlet, @@ -10,7 +10,7 @@ import { } from "@tanstack/react-router"; import { MockLayout } from "../components/mock-layout"; import { defaultWorkspaceId } from "../lib/env"; -import { handoffWorkbenchClient } from "../lib/workbench"; +import { getHandoffWorkbenchClient, resolveRepoRouteHandoffId } from "../lib/workbench"; const rootRoute = createRootRoute({ component: RootLayout, @@ -74,18 +74,27 @@ function WorkspaceLayoutRoute() { function WorkspaceRoute() { const { workspaceId } = workspaceRoute.useParams(); + const client = getHandoffWorkbenchClient(workspaceId); useEffect(() => { setFrontendErrorContext({ workspaceId, handoffId: undefined, }); }, [workspaceId]); - return ; + return ( + + ); } function HandoffRoute() { const { workspaceId, handoffId } = handoffRoute.useParams(); const { sessionId } = handoffRoute.useSearch(); + const client = getHandoffWorkbenchClient(workspaceId); useEffect(() => { setFrontendErrorContext({ workspaceId, @@ -93,11 +102,24 @@ function HandoffRoute() { repoId: undefined, }); }, [handoffId, workspaceId]); - return ; + return ( + + ); } function RepoRoute() { const { workspaceId, repoId } = repoRoute.useParams(); + const client = getHandoffWorkbenchClient(workspaceId); + const snapshot = useSyncExternalStore( + client.subscribe.bind(client), + client.getSnapshot.bind(client), + client.getSnapshot.bind(client), + ); useEffect(() => { setFrontendErrorContext({ workspaceId, @@ -105,9 +127,7 @@ function RepoRoute() { repoId, }); }, [repoId, workspaceId]); - const activeHandoffId = handoffWorkbenchClient.getSnapshot().handoffs.find( - (handoff) => handoff.repoId === repoId, - )?.id; + const activeHandoffId = resolveRepoRouteHandoffId(snapshot, repoId); if (!activeHandoffId) { return ( { setEditingField(field); @@ -197,13 +199,13 @@ const TranscriptPanel = memo(function TranscriptPanel({ } if (field === "title") { - void handoffWorkbenchClient.renameHandoff({ handoffId: handoff.id, value }); + void client.renameHandoff({ handoffId: handoff.id, value }); } else { - void handoffWorkbenchClient.renameBranch({ handoffId: handoff.id, value }); + void client.renameBranch({ handoffId: handoff.id, value }); } setEditingField(null); }, - [editValue, handoff.id], + [client, editValue, handoff.id], ); const updateDraft = useCallback( @@ -212,14 +214,14 @@ const TranscriptPanel = memo(function TranscriptPanel({ return; } - void handoffWorkbenchClient.updateDraft({ + void client.updateDraft({ handoffId: handoff.id, tabId: promptTab.id, text: nextText, attachments: nextAttachments, }); }, - [handoff.id, promptTab], + [client, handoff.id, promptTab], ); const sendMessage = useCallback(() => { @@ -230,24 +232,24 @@ const TranscriptPanel = memo(function TranscriptPanel({ onSetActiveTabId(promptTab.id); onSetLastAgentTabId(promptTab.id); - void handoffWorkbenchClient.sendMessage({ + void client.sendMessage({ handoffId: handoff.id, tabId: promptTab.id, text, attachments, }); - }, [attachments, draft, handoff.id, onSetActiveTabId, onSetLastAgentTabId, promptTab]); + }, [attachments, client, draft, handoff.id, onSetActiveTabId, onSetLastAgentTabId, promptTab]); const stopAgent = useCallback(() => { if (!promptTab) { return; } - void handoffWorkbenchClient.stopAgent({ + void client.stopAgent({ handoffId: handoff.id, tabId: promptTab.id, }); - }, [handoff.id, promptTab]); + }, [client, handoff.id, promptTab]); const switchTab = useCallback( (tabId: string) => { @@ -257,7 +259,7 @@ const TranscriptPanel = memo(function TranscriptPanel({ onSetLastAgentTabId(tabId); const tab = handoff.tabs.find((candidate) => candidate.id === tabId); if (tab?.unread) { - void handoffWorkbenchClient.setSessionUnread({ + void client.setSessionUnread({ handoffId: handoff.id, tabId, unread: false, @@ -266,14 +268,14 @@ const TranscriptPanel = memo(function TranscriptPanel({ onSyncRouteSession(handoff.id, tabId); } }, - [handoff.id, handoff.tabs, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], + [client, handoff.id, handoff.tabs, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], ); const setTabUnread = useCallback( (tabId: string, unread: boolean) => { - void handoffWorkbenchClient.setSessionUnread({ handoffId: handoff.id, tabId, unread }); + void client.setSessionUnread({ handoffId: handoff.id, tabId, unread }); }, - [handoff.id], + [client, handoff.id], ); const startRenamingTab = useCallback( @@ -305,13 +307,13 @@ const TranscriptPanel = memo(function TranscriptPanel({ return; } - void handoffWorkbenchClient.renameSession({ + void client.renameSession({ handoffId: handoff.id, tabId: editingSessionTabId, title: trimmedName, }); cancelTabRename(); - }, [cancelTabRename, editingSessionName, editingSessionTabId, handoff.id]); + }, [cancelTabRename, client, editingSessionName, editingSessionTabId, handoff.id]); const closeTab = useCallback( (tabId: string) => { @@ -326,9 +328,9 @@ const TranscriptPanel = memo(function TranscriptPanel({ } onSyncRouteSession(handoff.id, nextTabId); - void handoffWorkbenchClient.closeTab({ handoffId: handoff.id, tabId }); + void client.closeTab({ handoffId: handoff.id, tabId }); }, - [activeTabId, handoff.id, handoff.tabs, lastAgentTabId, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], + [activeTabId, client, handoff.id, handoff.tabs, lastAgentTabId, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession], ); const closeDiffTab = useCallback( @@ -346,12 +348,12 @@ const TranscriptPanel = memo(function TranscriptPanel({ const addTab = useCallback(() => { void (async () => { - const { tabId } = await handoffWorkbenchClient.addTab({ handoffId: handoff.id }); + const { tabId } = await client.addTab({ handoffId: handoff.id }); onSetLastAgentTabId(tabId); onSetActiveTabId(tabId); onSyncRouteSession(handoff.id, tabId); })(); - }, [handoff.id, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession]); + }, [client, handoff.id, onSetActiveTabId, onSetLastAgentTabId, onSyncRouteSession]); const changeModel = useCallback( (model: ModelId) => { @@ -359,13 +361,13 @@ const TranscriptPanel = memo(function TranscriptPanel({ throw new Error(`Unable to change model for handoff ${handoff.id} without an active prompt tab`); } - void handoffWorkbenchClient.changeModel({ + void client.changeModel({ handoffId: handoff.id, tabId: promptTab.id, model, }); }, - [handoff.id, promptTab], + [client, handoff.id, promptTab], ); const addAttachment = useCallback( @@ -551,17 +553,18 @@ const TranscriptPanel = memo(function TranscriptPanel({ }); interface MockLayoutProps { + client: HandoffWorkbenchClient; workspaceId: string; selectedHandoffId?: string | null; selectedSessionId?: string | null; } -export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }: MockLayoutProps) { +export function MockLayout({ client, workspaceId, selectedHandoffId, selectedSessionId }: MockLayoutProps) { const navigate = useNavigate(); const viewModel = useSyncExternalStore( - handoffWorkbenchClient.subscribe.bind(handoffWorkbenchClient), - handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient), - handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient), + client.subscribe.bind(client), + client.getSnapshot.bind(client), + client.getSnapshot.bind(client), ); const handoffs = viewModel.handoffs ?? []; const projects = viewModel.projects ?? []; @@ -668,7 +671,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } const title = window.prompt("Optional handoff title", "")?.trim() || undefined; const branch = window.prompt("Optional branch name", "")?.trim() || undefined; - const { handoffId, tabId } = await handoffWorkbenchClient.createHandoff({ + const { handoffId, tabId } = await client.createHandoff({ repoId, task, model: "gpt-4o", @@ -684,7 +687,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } search: { sessionId: tabId ?? undefined }, }); })(); - }, [activeHandoff?.repoId, navigate, viewModel.repos, workspaceId]); + }, [activeHandoff?.repoId, client, navigate, viewModel.repos, workspaceId]); const openDiffTab = useCallback( (path: string) => { @@ -726,8 +729,8 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } ); const markHandoffUnread = useCallback((id: string) => { - void handoffWorkbenchClient.markHandoffUnread({ handoffId: id }); - }, []); + void client.markHandoffUnread({ handoffId: id }); + }, [client]); const renameHandoff = useCallback( (id: string) => { @@ -746,9 +749,9 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } return; } - void handoffWorkbenchClient.renameHandoff({ handoffId: id, value: trimmedTitle }); + void client.renameHandoff({ handoffId: id, value: trimmedTitle }); }, - [handoffs], + [client, handoffs], ); const renameBranch = useCallback( @@ -768,24 +771,31 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } return; } - void handoffWorkbenchClient.renameBranch({ handoffId: id, value: trimmedBranch }); + void client.renameBranch({ handoffId: id, value: trimmedBranch }); }, - [handoffs], + [client, handoffs], ); const archiveHandoff = useCallback(() => { if (!activeHandoff) { throw new Error("Cannot archive without an active handoff"); } - void handoffWorkbenchClient.archiveHandoff({ handoffId: activeHandoff.id }); - }, [activeHandoff]); + void client.archiveHandoff({ handoffId: activeHandoff.id }); + }, [activeHandoff, client]); const publishPr = useCallback(() => { if (!activeHandoff) { throw new Error("Cannot publish PR without an active handoff"); } - void handoffWorkbenchClient.publishPr({ handoffId: activeHandoff.id }); - }, [activeHandoff]); + void client.publishPr({ handoffId: activeHandoff.id }); + }, [activeHandoff, client]); + + const pushHandoff = useCallback(() => { + if (!activeHandoff) { + throw new Error("Cannot push without an active handoff"); + } + void client.pushHandoff({ handoffId: activeHandoff.id }); + }, [activeHandoff, client]); const revertFile = useCallback( (path: string) => { @@ -804,18 +814,20 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId } : current[activeHandoff.id] ?? null, })); - void handoffWorkbenchClient.revertFile({ + void client.revertFile({ handoffId: activeHandoff.id, path, }); }, - [activeHandoff, lastAgentTabIdByHandoff], + [activeHandoff, client, lastAgentTabIdByHandoff], ); if (!activeHandoff) { return ( diff --git a/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx b/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx index ae8a1c0..d8927f1 100644 --- a/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx +++ b/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx @@ -15,6 +15,49 @@ import { import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui"; import { type FileTreeNode, type Handoff, diffTabId } from "./view-model"; +const StatusCard = memo(function StatusCard({ + label, + value, + mono = false, +}: { + label: string; + value: string; + mono?: boolean; +}) { + const [css, theme] = useStyletron(); + + return ( +
+ + {label} + +
+ {value} +
+
+ ); +}); + const FileTree = memo(function FileTree({ nodes, depth, @@ -106,6 +149,7 @@ export const RightSidebar = memo(function RightSidebar({ activeTabId, onOpenDiff, onArchive, + onPush, onRevertFile, onPublishPr, }: { @@ -113,6 +157,7 @@ export const RightSidebar = memo(function RightSidebar({ activeTabId: string | null; onOpenDiff: (path: string) => void; onArchive: () => void; + onPush: () => void; onRevertFile: (path: string) => void; onPublishPr: () => void; }) { @@ -121,7 +166,12 @@ export const RightSidebar = memo(function RightSidebar({ const contextMenu = useContextMenu(); const changedPaths = useMemo(() => new Set(handoff.fileChanges.map((file) => file.path)), [handoff.fileChanges]); const isTerminal = handoff.status === "archived"; + const canPush = !isTerminal && Boolean(handoff.branch); const pullRequestUrl = handoff.pullRequest != null ? `https://github.com/${handoff.repoName}/pull/${handoff.pullRequest.number}` : null; + const pullRequestStatus = + handoff.pullRequest == null + ? "Not published" + : `#${handoff.pullRequest.number} ${handoff.pullRequest.status === "draft" ? "Draft" : "Ready"}`; const copyFilePath = useCallback(async (path: string) => { try { @@ -183,6 +233,7 @@ export const RightSidebar = memo(function RightSidebar({ {pullRequestUrl ? "Open PR" : "Publish PR"}