mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 04:03:31 +00:00
feat(factory): finish workbench milestone pass
This commit is contained in:
parent
bf282199b5
commit
49cba9e6c2
137 changed files with 819 additions and 338 deletions
|
|
@ -17,7 +17,7 @@ coverage/
|
|||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
.openhandoff/
|
||||
.sandbox-agent-factory/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
BIN
.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm
Normal file
BIN
.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-shm
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal
Normal file
BIN
.openhandoff/backend/sqlite/handoff/1513aa2dffb2aeb1.sqlite-wal
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/history/2ee2a83a8f00bf51.sqlite
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/history/83fb73090cf32cca.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/history/83fb73090cf32cca.sqlite
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/project/27bd62398cfef512.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/project/27bd62398cfef512.sqlite
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/project/6d0adb3e91e6d8af.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/project/6d0adb3e91e6d8af.sqlite
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/project/86bea07dcf41d624.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/project/86bea07dcf41d624.sqlite
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/workspace/57c45274e0331bab.sqlite
Normal file
Binary file not shown.
BIN
.openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite
Normal file
BIN
.openhandoff/backend/sqlite/workspace/d506cab654089c0a.sqlite
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<void> }) => Promise<void>;
|
||||
|
|
@ -75,7 +77,11 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
|||
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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export interface ActorSqliteDbOptions<TSchema extends Record<string, unknown>> {
|
|||
/**
|
||||
* Override base directory for per-actor SQLite files.
|
||||
*
|
||||
* Default: `<cwd>/.openhandoff/backend/sqlite`
|
||||
* Default: `<cwd>/.sandbox-agent-factory/backend/sqlite`
|
||||
*/
|
||||
baseDir?: string;
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ export function actorSqliteDb<TSchema extends Record<string, unknown>>(
|
|||
}) as unknown as DatabaseProvider<any & RawAccess>;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ProviderId } from "@openhandoff/shared";
|
||||
import type { ProviderId } from "@sandbox-agent/factory-shared";
|
||||
|
||||
export interface ProviderCapabilities {
|
||||
remote: boolean;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<vo
|
|||
const client = createBackendClientFromConfig(config);
|
||||
const renderer = await createCliRenderer({ exitOnCtrlC: false });
|
||||
const text = new TextRenderable(renderer, {
|
||||
id: "openhandoff-switch",
|
||||
id: "factory-switch",
|
||||
content: "Loading..."
|
||||
});
|
||||
text.fg = themeResolution.theme.text;
|
||||
|
|
|
|||
|
|
@ -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, resolveWorkspaceId, type AppConfig } from "@openhandoff/shared";
|
||||
import { ConfigSchema, resolveWorkspaceId, 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)) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ vi.mock("node:child_process", async () => {
|
|||
});
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
1
factory/packages/client/src/backend.ts
Normal file
1
factory/packages/client/src/backend.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./backend-client.js";
|
||||
|
|
@ -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<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
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<void> {
|
||||
this.updateHandoff(input.handoffId, (handoff) => ({
|
||||
...handoff,
|
||||
updatedAtMs: nowMs(),
|
||||
}));
|
||||
}
|
||||
|
||||
async revertFile(input: HandoffWorkbenchDiffInput): Promise<void> {
|
||||
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<string, HandoffWorkbenchClient>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
await this.backend.runAction(this.workspaceId, input.handoffId, "push");
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async revertFile(input: HandoffWorkbenchDiffInput): Promise<void> {
|
||||
await this.backend.revertWorkbenchFile(this.workspaceId, input);
|
||||
await this.refresh();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
archiveHandoff(input: HandoffWorkbenchSelectInput): Promise<void>;
|
||||
publishPr(input: HandoffWorkbenchSelectInput): Promise<void>;
|
||||
pushHandoff(input: HandoffWorkbenchSelectInput): Promise<void>;
|
||||
revertFile(input: HandoffWorkbenchDiffInput): Promise<void>;
|
||||
updateDraft(input: HandoffWorkbenchUpdateDraftInput): Promise<void>;
|
||||
sendMessage(input: HandoffWorkbenchSendMessageInput): Promise<void>;
|
||||
|
|
@ -49,7 +50,7 @@ export function createHandoffWorkbenchClient(
|
|||
options: CreateHandoffWorkbenchClientOptions,
|
||||
): HandoffWorkbenchClient {
|
||||
if (options.mode === "mock") {
|
||||
return getSharedMockWorkbenchClient();
|
||||
return getMockWorkbenchClient(options.workspaceId);
|
||||
}
|
||||
|
||||
if (!options.backend) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
1
factory/packages/client/src/workbench.ts
Normal file
1
factory/packages/client/src/workbench.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./workbench-client.js";
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function seedSandboxFile(workspaceId: string, handoffId: string, filePath: string, content: string): Promise<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<T>(
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
128
factory/packages/client/test/workbench-client.test.ts
Normal file
128
factory/packages/client/test/workbench-client.test.ts
Normal file
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@openhandoff/frontend-errors",
|
||||
"name": "@sandbox-agent/factory-frontend-errors",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue