Agent instruction file only. This is not user-facing documentation or a getting-started guide. Canonical public documentation lives in
apps/www/src/content/docs/docs/.
A serverless monorepo platform for ephemeral AI coding agent environments on Cloudflare Workers + Hetzner Cloud VMs.
apps/
├── api/ # Cloudflare Worker API (Hono)
├── web/ # Control plane UI (React + Vite)
├── www/ # Marketing website, blog & docs (Astro + Starlight) — simple-agent-manager.org
└── tail-worker/ # Cloudflare Tail Worker (observability)
packages/
├── shared/ # Shared types and utilities
├── providers/ # Cloud provider abstraction (Hetzner, Scaleway)
├── terminal/ # Shared terminal component
├── cloud-init/ # Cloud-init template generator
├── acp-client/ # Shared ACP React components (MessageBubble, MessageActions, AudioPlayer)
├── ui/ # Design system tokens and shared UI components
└── vm-agent/ # Go VM agent (PTY, WebSocket, ACP, MCP tool endpoints)
tasks/ # Task tracking (backlog -> active -> archive)
specs/ # Feature specifications
pnpm install # Install dependencies
pnpm build # Build all packages
pnpm test # Run tests
pnpm typecheck # Type check
pnpm lint # Lint
pnpm format # FormatBuild packages in dependency order: shared -> providers -> cloud-init -> api / web
pnpm --filter @simple-agent-manager/shared build
pnpm --filter @simple-agent-manager/providers build
pnpm --filter @simple-agent-manager/api buildThis monorepo has TWO separate web surfaces. Do NOT confuse them:
| Surface | Directory | Domain | Stack | What it is |
|---|---|---|---|---|
| Marketing website | apps/www/ |
simple-agent-manager.org |
Astro + Starlight | Public website, landing pages, blog, docs |
| App (control plane) | apps/web/ |
app.simple-agent-manager.org |
React + Vite | Authenticated SaaS UI (dashboard, projects, settings) |
When the user mentions website, marketing, landing page, blog, docs site, or public pages → look in apps/www/.
When the user mentions app, dashboard, projects, settings, or UI → look in apps/web/.
Local-first, Cloudflare-integrated. Prove as much of a feature as you can locally before touching staging. Local iteration takes seconds; staging iteration takes minutes and burns VM quota. Staging is for things that genuinely require real infrastructure (OAuth callbacks, DNS, VM provisioning, edge TLS) — not for discovering whether your code compiles.
- Prototype and test locally first — unit tests, Miniflare integration tests, local Vite dev server, Playwright visual audits. Hybrid loops (local UI against staging API, or local API against staging VM agent) are encouraged. See
.claude/rules/29-local-first-debugging.md. Prototype artifacts are not production deliverables by default; do not ship throwaway prototype pages, demo routes, fixture-backed UI, or scaffolded experiments unless the user explicitly asks to ship the prototype itself. - Deploy to staging only when local verification is exhausted — when the remaining work genuinely needs real OAuth, DNS, or VMs. Partial-feature staging deploys are fine for end-to-end plumbing while the rest is still developed locally. Staging deploys take ~7 minutes via
gh workflow run deploy-staging.yml. - Query staging directly via Cloudflare API — use
$CF_TOKENto query D1 (SQL), read/write KV, check DNS records, and inspect Workers. This is the fastest way to verify deploys, debug issues, and understand staging state. Always check infrastructure state via CF API before guessing at fixes. See.claude/rules/32-cf-api-debugging.mdfor the full cheat sheet. - When something fails on staging, QUERY THEN READ LOGS before changing any code — first query D1/KV/DNS via CF API to understand the data state, then use
wrangler tail,/admin/logs,/admin/errors, the Node detail page's log stream,journalctl -u vm-agentvia SSH,docker logsfor containers. Never guess-and-redeploy. See.claude/rules/29-local-first-debugging.mdfor the log location matrix. - Merge to main — in this canonical repository, successful
mainCI triggers production deployment. Self-host forks update by manually running Deploy Production onmain.
Full local-development guide: apps/www/src/content/docs/docs/guides/local-development.md.
Merging to main in the canonical repository automatically deploys to production after CI succeeds. Self-host forks do not update from a push alone; operators manually run Deploy Production on their fork's main branch when they want to deploy.
- CI (
ci.yml): lint, typecheck, test, build on pull requests and canonicalmainpushes; forkmainpushes are intentionally skipped - Deploy Staging (
deploy-staging.yml): manual trigger only (workflow_dispatch) — agents trigger this explicitly during/doPhase 6 - Deploy Production (
deploy.yml): full Pulumi + Wrangler deployment after successful canonicalmainCI, or manualworkflow_dispatchfor self-host forks - Teardown (
teardown.yml): manual only — destroys all resources - Generated platform secrets: deployment-owned signing/encryption keys are generated and persisted by Pulumi when practical, then copied to Worker secrets. Do not add manual GitHub Environment prerequisites for values SAM can safely create itself; GitHub secrets for generated keys are override/rotation paths only.
Staging deployment is manual — triggered via gh workflow run deploy-staging.yml --ref <branch>. Agents executing the /do workflow MUST deploy to staging and verify the live app before merging. A failed staging deploy blocks merge just like a failed test. Before triggering a deployment, check for existing active runs and wait at least 5 minutes if one is in progress. If the deploy fails due to missing secrets or configuration (not code), alert the user immediately — do not skip verification. See .claude/rules/13-staging-verification.md.
Staging verification means the feature WORKS — not that pages load, not that config endpoints respond, not that the UI renders. The actual feature, exercised as an end user would, must complete successfully with ZERO errors. If the feature errors on staging for ANY reason (missing binding, wrong toolchain version, unconfigured service), do NOT merge — alert the user immediately. Never rationalize a staging error as "expected." See .claude/rules/30-never-ship-broken-features.md.
After merging ANY PR to main in this canonical repository, agents MUST monitor the Deploy Production workflow to completion. If the deploy fails, alert the user immediately with the failure reason and whether it requires human intervention. Do NOT silently finish the task when the deploy fails — a merged PR is not shipped until the deploy succeeds. See the /do workflow Phase 7b for the full procedure.
Production data loss is catastrophic and irreversible. Multiple deterministic gates prevent it:
| Gate | Runs in | What it catches |
|---|---|---|
pnpm quality:migration-safety |
CI (every PR) | DROP TABLE on CASCADE parents, DELETE without WHERE, PRAGMA foreign_keys=OFF, UPDATE without WHERE, any DROP TABLE in new migrations |
pnpm quality:do-migration-safety |
CI (every PR) | DROP TABLE, DELETE without WHERE, UPDATE without WHERE in Durable Object SQLite migrations (no recovery mechanism) |
| Pre-migration D1 backup | Deploy pipeline | Creates time-travel bookmark + explicit backup before every migration run |
| Post-migration row count verification | Deploy pipeline | Compares row counts before/after migrations; blocks deploy if >50% data loss detected in any table |
| D1 Time Travel Restore | Manual workflow | Point-in-time recovery for D1 databases (30-day window). See d1-restore.yml |
Migration rules: See .claude/rules/31-migration-safety.md. NEVER use DROP TABLE on any table with CASCADE children. Use ALTER TABLE ADD COLUMN instead of table recreation.
- Workspace: AI coding environment (VM + devcontainer + Claude Code)
- Node: VM host that runs multiple workspaces
- Provider: Cloud infrastructure abstraction (currently Hetzner only)
- Project: Primary organizational unit linking a GitHub repo to workspaces, chat sessions, tasks, and activity
- ProjectData DO: Per-project Durable Object with embedded SQLite for chat sessions, messages, activity events, and ACP sessions (spec 027). Accessed via
env.PROJECT_DATA.idFromName(projectId) - NodeLifecycle DO: Per-node Durable Object managing warm pool state machine (active → warm → destroying). Accessed via
env.NODE_LIFECYCLE.idFromName(nodeId). Handles idle timeout alarms; actual infrastructure teardown delegated to cron sweep. - Warm Node Pooling: After task completion, auto-provisioned nodes enter "warm" state for 30 min (configurable via
NODE_WARM_TIMEOUT_MS) for fast reuse. Three-layer defense against orphans: DO alarm + cron sweep + max lifetime. - Task Runner: Autonomous task execution — selects/provisions nodes, creates workspaces, runs agents, cleans up. VM size precedence: explicit override > project default > platform default.
- Lifecycle Control: Workspaces/nodes stopped, restarted, or deleted explicitly via API/UI
The root domain does NOT serve any application. Always use subdomains:
| Destination | URL Pattern |
|---|---|
| Web UI | https://app.${BASE_DOMAIN}/... |
| API | https://api.${BASE_DOMAIN}/... |
| Workspace | https://ws-${id}.${BASE_DOMAIN} |
| Workspace Port | https://ws-${id}--${port}.${BASE_DOMAIN} |
- User-facing redirects ->
app.${BASE_DOMAIN}(NEVER bare${BASE_DOMAIN}) - API-to-API references ->
api.${BASE_DOMAIN} - Relative redirects in API worker are WRONG — they resolve to the API subdomain
GitHub Actions secret names cannot start with GITHUB_*, so GitHub App secrets use GH_* prefix. The deployment script (configure-secrets.sh) maps them to GITHUB_* Worker secrets.
| Context | Prefix | Example |
|---|---|---|
| GitHub Environment | GH_ |
GH_CLIENT_ID |
| Worker runtime / .env | GITHUB_ |
GITHUB_CLIENT_ID |
Full env var reference: use the env-reference skill or see apps/api/.env.example.
Environment-specific [env.*] sections are NOT checked into the repository. They are generated at deploy time by scripts/deploy/sync-wrangler-config.ts from Pulumi outputs + the top-level config. When adding ANY new binding to wrangler.toml, add it to the top-level section only. The sync script copies static bindings (Durable Objects, AI, migrations) and generates dynamic bindings (D1, KV, R2, worker name, routes, tail_consumers) automatically. The CI quality check (pnpm quality:wrangler-bindings) verifies that no env sections are committed and that required binding types are present at the top level. See .claude/rules/07-env-and-urls.md for details.
- BYOC (Bring-Your-Own-Cloud): Self-hosters and users may bring their own Hetzner tokens, encrypted per-user. This is the model for self-hosted deployments and BYO-key users. However, SAM's own deployment (staging, and the platform-hosted / zero-config mode) DOES have an enabled platform-level cloud credential (
platform_credentials,provider=hetzner,credential_type=cloud-provider,is_enabled=1). Provider resolution falls back user credential → platform credential, so a user does NOT need their own cloud credential for SAM to provision workspaces or deployment nodes. NEVER treat a missing user cloud credential — e.g. a smoke/test user stuck at the cloud-onboarding wizard, or zero active workspaces — as a provisioning or staging-verification blocker. The platform Hetzner credential provisions VMs regardless. Verify before ever reporting such a blocker: D1SELECT id FROM platform_credentials WHERE credential_type='cloud-provider' AND is_enabled=1. - User credentials encrypted per-user in the database — NOT stored as env vars or Worker secrets. Public security architecture documentation lives in
apps/www/src/content/docs/docs/architecture/security.md. - Platform secrets (ENCRYPTION_KEY and purpose-specific overrides, JWT keys, deploy signing keys, CF_API_TOKEN) are Cloudflare Worker secrets set during deployment. SAM-owned generated keys should be Pulumi-managed by default, with GitHub secret overrides only when manual rotation is explicitly needed.
- Canonical IDs for identity — use
workspaceId,nodeId,sessionIdfor all machine-critical operations (storage, routing, lifecycle). Human-readable labels are for UX/logging only and MUST be treated as mutable and non-unique. - Hybrid D1 + Durable Object storage — D1 for cross-project queries (dashboard, tasks, users); per-project DOs for write-heavy data (chat sessions, messages, activity events). See
apps/www/src/content/docs/docs/architecture/overview.md.
- Always use worktrees and PRs — never commit directly to main. Create a feature branch in a git worktree and open a PR.
- No late direct-to-main fixes — review fixes, CI fixes, staging fixes, and post-merge deploy fixes still go through a branch and PR. The only
/dodirect-to-main exception is the initial task file. - Push early and often — environments are ephemeral. Unpushed work can be lost at any time.
- Pull and rebase frequently — before starting work and before pushing, run
git fetch origin && git rebase origin/mainto stay current and avoid conflicts. - After pushing, check CI and fix any failures before moving on.
- Fix all build/lint errors before pushing — even pre-existing ones
- No dead code — if code is no longer referenced, remove it in the same change
- Capability tests required — every multi-component feature needs at least one test that exercises the complete happy path across system boundaries. Component tests alone are not sufficient. See
.claude/rules/10-e2e-verification.md. - Verify assumptions, don't trust documentation — when specs or docs say "existing X works," verify with a test or manual check before building on it.
- Cite code paths in behavioral docs — when documenting what the system does, cite specific functions. Never write "X happens" without a code reference. Mark unimplemented behavior as "intended" not present tense.
- Diagrams in markdown — use Mermaid (
\``mermaid) for all diagrams in.md` files. The markdown renderer supports Mermaid natively. - Subagents live in
.claude/agents/; Codex skills in.agents/skills/ - Playwright screenshots go in
.codex/tmp/playwright-screenshots/(gitignored) - Ephemeral scratch files go in
.tmp/— debug dumps, downloaded logs, scratch notes, generated fixtures, anything that must NOT be committed. The directory is gitignored (see.tmp/README.md). Never drop temporary artifacts in the repo root or package directories. - No strategy docs in this repo — this is a public repository; business/marketing/competitive strategy documents are intentionally kept out of it. Do not create a
strategy/directory. - Playwright visual audit required for UI changes — any PR touching
apps/web/,packages/ui/, orpackages/terminal/must run Playwright visual tests with diverse mock data on mobile (375px) and desktop (1280px) viewports. See.claude/rules/17-ui-visual-testing.md. - No duplicate UI controls — before adding any new settings control or form field, search for existing controls managing the same API field. Consolidate into one canonical location. See
.claude/rules/24-no-duplicate-ui-controls.md. - Stale-while-revalidate UI — context provider values must be memoized (ESLint-enforced), loading spinners may only gate rendering when there is no data yet, refetches must never unmount visible content, and new fetch surfaces in
apps/web/use TanStack Query. See.claude/rules/48-stale-while-revalidate-ui.md.
Agents support three provider modes (stored as providerMode in agent_settings):
user-api-key(default): User's own API key (ANTHROPIC_API_KEY,OPENAI_API_KEY, etc.) injected at workspace boot. No platform proxy involved.oauth: OAuth token from the provider's subscription plan (e.g., Claude Max/Pro viaclaude setup-token). Injected asCLAUDE_CODE_OAUTH_TOKEN.sam: Platform-managed AI proxy. The workspace receives a__platform_proxy__sentinel as its API key and routes all LLM traffic through the SAM AI proxy (/ai/v1/*), which handles billing, usage tracking, rate limiting, and budget enforcement via Cloudflare AI Gateway.
Users select their provider mode per-agent in Settings → Agent Settings. The sam mode requires explicit opt-in — it is never auto-selected.
- Staging authentication: Use Playwright browser auth with the staging smoke/API token in
SAM_PLAYWRIGHT_PRIMARY_USER. In the Playwright browser context, POST tohttps://api.sammy.party/api/auth/token-loginwith body{ "token": "<value>" }, verify a 200 response, then navigate tohttps://app.sammy.party. Do not rely onSAM_API_URL; it may point at production, where staging tokens correctly fail with401 Invalid token. See.claude/rules/13-staging-verification.mdfor full procedure. - Production authentication: Use GitHub OAuth credentials at
/workspaces/.tmp/secure/demo-credentials.md(outside repo) - Live test cleanup required: delete test workspaces/nodes after verification
- Staging verification required for every code PR — see
.claude/rules/13-staging-verification.md - See
.claude/rules/02-quality-gates.mdfor full testing requirements
When you discover bugs or errors during testing — even if unrelated to your current task — file them as backlog tasks immediately so they don't get lost:
- Create
tasks/backlog/YYYY-MM-DD-descriptive-name.md - Include: Problem description, Context (where/when discovered), Acceptance Criteria checklist
- Continue with your current work
If the bug is blocking the current task or is a small adjacent fix, fix it in the current branch with evidence. Otherwise file it and keep the assigned work moving.
- Build errors: Run builds in dependency order (see Build Order above)
- Test failures: Check Miniflare bindings are configured in
vitest.config.ts - Type errors: Run
pnpm typecheckfrom root to see all issues - Staging issues: Query staging state directly via
$CF_TOKENand the Cloudflare API — D1 SQL queries, KV reads, DNS checks. See.claude/rules/32-cf-api-debugging.mdfor copy-paste commands. Always query before guessing.
Tasks tracked as markdown in tasks/ (backlog -> active -> archive). See tasks/README.md for conventions.
Dispatching tasks: When dispatching tasks to other agents, always instruct them to use the /do skill in prose, for example Execute this task using the /do skill. Never start a dispatched task description with /do or any slash command; Codex treats that as CLI slash-command syntax and can reject the prompt before SAM bootstrap instructions are processed. Verify the task actually started with the requested profile and title. Do not wait on failed, queued, missing, or wrong-profile sessions. See .claude/rules/09-task-tracking.md.
Read-only investigations: PR status, PR history, task status, and diagnostic questions are read-only by default. Use SAM MCP, GitHub, logs, and local evidence in the current session. Do not create task files, branches, commits, or PRs unless the user asks for code/config changes or durable artifacts.
Failed task retries: Before retrying or redispatching a failed SAM task, inspect the failed task/session and check for active duplicate work with the same prompt, output branch, branch, or title. Do not blindly resubmit the same prompt after no-workspace/startup failures or transient provider failures. See .claude/rules/09-task-tracking.md for profile, skill, and task-mode validation details.
Agent profile defaults: When changing profile setup or onboarding, fresh installs should not seed multiple provider-specific built-in profiles. Prefer a setup wizard, templates, or at most one conversational default so users learn profiles intentionally instead of inheriting clutter.
Memory and ideas: Keep SAM knowledge, ideas, and policies current when human feedback or shipped work changes what future agents should believe. Do not mark ideas complete unless they are merged or otherwise verifiably shipped. See .claude/rules/38-agent-feedback-and-memory.md.
- TypeScript 5.x (Worker/Web), Go 1.24+ (VM Agent)
- Hono (API framework), Drizzle ORM (D1), React 19 + Vite (Web)
- Cloudflare Workers SDK (Durable Objects, D1, KV, R2, Workers AI)
- Tailwind CSS v4 (Web), Astro + Starlight (Marketing site)
- @mastra/core (AI agent orchestration), workers-ai-provider (Workers AI bridge)
- ACP Go SDK,
creack/pty+gorilla/websocket(VM Agent)
Use the /changelog skill for structured queries.
- origin-tag-injected-messages: SAM-injected prompt text (e.g. the
get_instructionsreminder) is no longer concatenated into the visible user prompt. It is passed separately asinjectedInstructionson the vm-agent create-session HTTP body (startAgentSessionOnNode→buildInitialPromptParams), emitted as a second ACP prompt block tagged_meta["sam.origin"]="system". The vm-agent honors that marker ONLY from a trusted control-plane prompt (HandlePrompt(..., trustedSource)) — browser viewer prompts have it stripped so a user cannot hide their own content. Origin propagates through the messagereport outbox (neworigincolumn), the ProjectData DOchat_messagestable (additive migration024-chat-message-origin, NULL/absent =user),ChatMessageResponse, and the livesession/updatebroadcast. The web collapsesorigin=systemuser messages behind a<details>"Show system context" disclosure (AcpConversationItemView).origin=systemcontent is excluded from user-content dedup, LIKE/FTS search (materialization filters it out ofchat_messages_grouped), topic auto-capture, and attention resolution. - chat-full-load-timeline-jump: Project chat loads the FULL conversation on open (
getSessionMessageLimitsplit into a small default page sizeCHAT_SESSION_MESSAGE_LIMITfor the 3s poll/load-more and a largeCHAT_SESSION_MESSAGE_MAXceiling the initial load requests; newDEFAULT_CHAT_SESSION_MESSAGE_MAX). The 30 MiB RPC size guard +hasMore"Load earlier" remain the fallback for oversized sessions. Timeline jump-to-message now anchors by messageId+timestamp with aloadUntilfallback (no more dead clicks), every entry kind (user message, status update, activity) is clickable, and the jumped-to message flashes (.sam-message-highlight). Removed the now-deadmessageIndex/messageIndexMapfrom the timeline. - skills-profile-override-layer: Skills as a first-class profile-override layer (skill → profile → project → platform); additive migration
0063_skills.sqladdsskills,skill_runtime_env_vars,skill_runtime_filesandtasks.skill_id/tasks.skill_hint/triggers.skill_id; shared mappers inprofile-fields.ts/profile-runtime-assets.ts; CRUD + runtime routes under/api/projects/:projectId/skills; resolution wired through task/trigger/MCP/dispatch + workspace runtime asset injection; Skills UI + chat/task skill selectors inapps/web - explicit-sam-provider-selection: Require explicit opt-in to SAM as AI provider via
providerMode: 'sam'; three-mode agent auth (user-api-key, oauth, sam); AI proxy auth gate on all endpoints including /models - compact-mode-lazy-load-tool-content: Chat compact mode strips tool content from RPC payload (80-90% reduction); lazy-loads on expand
- harness-track-d-integration-design: SAM-native harness architecture doc; Gemma 4 26B as default Workers AI model
- ai-proxy-universal-tracking: URL-path-based passthrough proxy for usage tracking without consuming auth headers
- user-ai-budget-controls: User-facing daily token budgets + monthly cost cap with 3-tier resolution
- anthropic-proxy-endpoint: Native Anthropic Messages API proxy through AI Gateway with Unified Billing
- user-ai-usage-dashboard: Per-user LLM usage dashboard from AI Gateway logs (by model, by day)
- cost-monitoring-dashboard: Admin cost dashboard aggregating LLM + compute costs with projections
- sam-observability-context-tools: SAM tools for searching task messages and browsing project codebases
- sam-agent-phase-a-tools: SAM orchestration tools (dispatch_task, create_mission, get_task_details)
- policy-propagation-phase4: Project policies with MCP tools + propagation to child tasks