diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 9d770c6..410de47 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -1,11 +1,14 @@ +# NOTE: automatic triggering is disabled — see DEPLOYMENT.md "Marketing site". +# The ghost-app Vercel project (VERCEL_PROJECT_ID below) currently has its Root +# Directory overridden to cloud/apps/web, which now serves the live cloud SaaS +# app at ghost.muharafiq.com. Because that override applies regardless of what +# changed in the push, running this workflow as-is would redeploy cloud/apps/web +# again, not public/, no matter what the steps below claim. Do not re-enable the +# push trigger or run this via workflow_dispatch until public/ has its own +# Vercel project (or domain) pointed at this directory, or is retired. name: Deploy Website on: - push: - branches: [main, master] - paths: - - "public/**" - - ".github/workflows/deploy-website.yml" workflow_dispatch: permissions: @@ -13,7 +16,7 @@ permissions: jobs: deploy: - name: Deploy to Vercel (ghost.muharafiq.com) + name: Deploy to Vercel (public/ — target currently misconfigured, see note above) runs-on: ubuntu-latest steps: @@ -62,11 +65,10 @@ jobs: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} run: | + echo "::warning::VERCEL_PROJECT_ID's Root Directory is overridden to cloud/apps/web, so this will deploy the cloud app, not public/. See the note at the top of this workflow file." echo "Deploying public/ to Vercel…" vercel deploy --prod --token="${VERCEL_TOKEN}" --yes - echo "Live: https://ghost.muharafiq.com" - name: Deployment summary run: | - echo "Deployed $(find public -type f | wc -l) files from public/" - echo "Site: https://ghost.muharafiq.com" + echo "Deployed $(find public -type f | wc -l) files from public/ (see the warning above about where this actually landed)" diff --git a/CLAUDE.md b/CLAUDE.md index 3a8fc3e..46b661e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,8 +106,11 @@ Build the five-part MVP in `cloud/`, in order: Built so far (Phase 1): the execution engine, the deterministic approval gate, per-step screenshots + verification, the hash-chained audit log, the run → approval → verify UI, and the agent HTTP/MCP surface (agents propose; humans -approve — `cloud/docs/AGENT_PLUGIN.md`). Recording (steps 1–2) is next. See -`cloud/docs/PHASE_1_PLAN.md` and `cloud/docs/CURSOR_HANDOFF.md`. +approve — `cloud/docs/AGENT_PLUGIN.md`). Phase 2 (recording → editable steps) is +in progress: the convert side (upload → deterministic compile → review) is +built and off by default; a Chrome extension (`cloud/apps/extension`) captures +the browser session. See `cloud/docs/PHASE_1_PLAN.md` and +`cloud/docs/CURSOR_HANDOFF.md` for current status. Required behavior (unchanged in spirit from the desktop trust pipeline): @@ -117,6 +120,49 @@ Required behavior (unchanged in spirit from the desktop trust pipeline): - verify each step's outcome; - write audit events (hash-chained) for every run and step. +### Cloud workspace layout & commands + +`cloud/` is a self-contained pnpm + Turborepo workspace — it does not share +tooling with the repo root. `cd cloud` before running anything below. + +```text +cloud/ + apps/ + web/ Next.js 15 (App Router) — UI + API + /api/agent/* → deployed to Vercel + worker/ Node worker: BullMQ consumers + Playwright execution → deployed as a container + mcp/ Stdio MCP bridge for Cursor/Claude (no approve tools) + extension/ Chrome extension — records a browser session for Phase 2 capture + packages/ + core/ Prisma schema, Zod step types, classifyStep (approval gate), audit chain, agent catalog +``` + +Quickstart: `cd cloud && pnpm demo` (writes `.env`, brings up Postgres/Redis, +migrates, installs Chromium, starts web + worker — idempotent, safe to rerun). +Manual steps and the two load-bearing env vars (`GHOST_ARTIFACT_DIR` must be an +absolute path shared by web+worker; `GHOST_SESSION_KEY` must decode to 32 bytes) +are in `cloud/README.md`. + +Validation from inside `cloud/`: + +```bash +pnpm typecheck # the real static gate — run this even if lint is clean +pnpm lint # apps/web only; worker/mcp/core have no lint script yet +pnpm test # ~90 of ~239 tests need DATABASE_URL set or they skip silently +pnpm build +``` + +`pnpm test` also needs `REDIS_URL` and `GHOST_SESSION_KEY` set (not just +`DATABASE_URL`) or the DB-gated tests run instead of skipping and fail on +status rather than on anything naming the missing var: without `REDIS_URL`, +rate-limited routes (e.g. invite acceptance) fail closed with 429; without +`GHOST_SESSION_KEY`, the worker skips session capture at an approval gate, so +every gated run refuses to resume and ends `INCIDENT`. Both read like product +bugs and are actually a missing local env var — `.github/workflows/cloud.yml` +sets all three for exactly this reason. + +Don't reach for the root-level `cargo`/`make` commands when working in `cloud/` +— they build the unrelated legacy desktop app. + ## Engineering rules Every meaningful operation should pass through: @@ -211,7 +257,7 @@ Current structure: src/ # Tauri desktop frontend (ES-module JS/HTML/CSS, bundled by Vite; main.js holds most UI logic; compression-review.js/.css is the split-out event-review timeline; src/public/ holds pass-through static assets) apps/macos/ # Ghost 2.0 native macOS app (SwiftUI): App/, Views/, Features/, Services/, RustBridge/, AppKitBridge/ — UI only; all trust decisions stay in the Rust core over a JSON stdin/stdout bridge (docs/legacy/native-macos-preview.md) native/macos/ # GhostAXHelper.swift — read-only macOS Accessibility helper (list_matches op) -public/ # marketing/download site (static vanilla JS with in-browser demos; ships Ghost.dmg / Ghost_Setup.exe under downloads/; auto-deployed to Vercel by deploy-website.yml) +public/ # marketing/download site for the legacy desktop app (static vanilla JS with in-browser demos; ships Ghost.dmg / Ghost_Setup.exe under downloads/); NOT currently deployed anywhere — deploy-website.yml's auto-trigger is disabled because the Vercel project it targets now serves cloud/apps/web at ghost.muharafiq.com instead (see DEPLOYMENT.md) src-tauri/ # Rust backend docs/ # planning and technical docs .github/workflows/ # CI (rust.yml), release (release.yml), site deploy (deploy-website.yml) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 35b61a7..137d940 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -26,12 +26,28 @@ Production sketch: Cloud CI workflow is staged at `cloud/ci/cloud.yml` until installed under `.github/workflows/`. -Details: `cloud/README.md`, `cloud/docs/CURSOR_HANDOFF.md`. +**Live today:** the production domain **`ghost.muharafiq.com`** points at the +`ghost-app` Vercel project with its Root Directory set to `cloud/apps/web` — it +serves the cloud SaaS app directly (deployed manually today via +`vercel --prod --scope muharafiq --cwd cloud/apps/web`, not by a checked-in CI +workflow). This is a deliberate change from the domain's original use as the +static marketing site — see "Marketing site" below. -## Marketing site +Details: `cloud/README.md`, `cloud/docs/CURSOR_HANDOFF.md`. -Static files in `public/` deploy via `.github/workflows/deploy-website.yml` to Vercel. -This is the public marketing surface — keep it aligned with the cloud product. +## Marketing site (currently not deployed anywhere) + +`public/` is a separate static site (vanilla JS, ships the legacy desktop +Ghost.dmg/Ghost_Setup.exe installers) for the superseded desktop product, not +the cloud SaaS. `.github/workflows/deploy-website.yml` still exists to deploy +it, but it targets the same `ghost-app` Vercel project that now serves +`cloud/apps/web` above — because Vercel's Root Directory override applies +regardless of which files actually changed, running this workflow today would +redeploy the cloud app again, not `public/`, despite its build log claiming +otherwise. Its automatic trigger is disabled for that reason (see the workflow +file). Before re-enabling it: either point it at a separate Vercel +project/domain for the legacy site, or retire `public/` outright if the legacy +desktop product no longer needs a public download page. ## Legacy desktop diff --git a/cloud/.env.example b/cloud/.env.example index d334935..240f15d 100644 --- a/cloud/.env.example +++ b/cloud/.env.example @@ -4,7 +4,16 @@ # Redis with the defaults below out of the box. # --------------------------------------------------------------------------- -# Postgres (matches cloud/docker-compose.yml) +# Postgres (matches cloud/docker-compose.yml). +# +# Both apps load THIS file — the workspace root one — via packages/core/src/env.ts. +# +# If 5432 is already taken by an unrelated Postgres (Homebrew, Postgres.app, +# another project), `pnpm demo` moves Ghost's to 55432 and rewrites this line. +# It probes by running a query as the ghost user, because a port that merely +# accepts connections is not evidence: a foreign Postgres answers the socket and +# then denies every query, which surfaces much later as unexplained 500s. +# `pnpm check` reports the same thing without changing anything. DATABASE_URL="postgresql://ghost:ghost@localhost:5432/ghost?schema=public" # Redis / BullMQ (matches cloud/docker-compose.yml) @@ -46,6 +55,14 @@ AUTH_GITHUB_SECRET="" AUTH_GOOGLE_ID="" AUTH_GOOGLE_SECRET="" +# Email magic-link sign-in via Resend (optional in dev, same reason). Needs +# the sending domain verified in Resend (SPF/DKIM DNS records) before real +# emails will deliver — see docs/DEPLOY.md. Setting this also activates the +# Prisma adapter (see auth.ts), which GitHub/Google reuse for account linking. +# Install: https://vercel.com/marketplace/resend, or https://resend.com directly. +RESEND_API_KEY="" +RESEND_EMAIL_DOMAIN="" + # S3-compatible object storage for run screenshots + recording traces. # Works with AWS S3, Cloudflare R2, or MinIO. Not required for local dev. # diff --git a/cloud/README.md b/cloud/README.md index 0db23ab..d86642b 100644 --- a/cloud/README.md +++ b/cloud/README.md @@ -90,6 +90,8 @@ cp .env.example .env # a working local config as-is # GHOST_ARTIFACT_DIR -> e.g. $PWD/.artifacts pnpm install # runs `prisma generate` via core postinstall docker compose up -d # Postgres :5432, Redis :6379 + # (set GHOST_PG_PORT / GHOST_REDIS_PORT if + # those ports are already taken) pnpm db:migrate # apply the Prisma schema pnpm --filter @ghost/worker exec playwright install chromium pnpm dev # web on http://localhost:3000 + worker @@ -116,6 +118,34 @@ pnpm --filter @ghost/web dev pnpm --filter @ghost/worker dev ``` +Both read `cloud/.env` directly (`packages/core/src/env.ts`), so either one +works on its own. A real environment variable always wins over the file, and in +deployment there is no `.env` at all. + +## When something is wrong + +```bash +pnpm check +``` + +Read-only; it names the problem rather than leaving you to infer it. Ghost +fails locally in two ways that look like nothing at all: + +- **No worker running.** The UI is fine, "Run" appears to work, and the run sits + there forever, because the process that executes runs is not up. `pnpm dev` + starts both; `pnpm --filter @ghost/worker dev` starts just the worker. +- **`DATABASE_URL` pointing at the wrong Postgres.** A Postgres that is merely + *listening* on 5432 is not Ghost's — Homebrew's, Postgres.app's, another + project's container will all accept the connection and deny the user. `pnpm + demo` probes with real credentials, moves to a free port if it must, and + repairs `.env`. + +A stalled run is no longer permanent either: the worker reclaims runs whose +lease expired (`apps/worker/src/jobs/reclaimRuns.ts`) on boot and every minute, +so a crash or a redeploy mid-run resumes from the journal instead of leaving a +row `RUNNING` forever. After five failed restarts it becomes an `INCIDENT` for a +human, rather than looping. + ## Smoke test (Phase 1) 1. Open http://localhost:3000 and sign in (dev-credentials accepts any email; @@ -144,7 +174,7 @@ and `turbo run lint` skips packages that define no `lint` script — silently, a with a green summary. Treat `typecheck` as the real static gate until the other three packages have configs. -A full green run is **424 tests**. Roughly 90 of them are gated on +A full green run is **430 tests**. Roughly 90 of them are gated on `Boolean(process.env.DATABASE_URL)` and **skip silently** without it — so a green run with no database covers none of the execution engine. If the worker suite reports 45 tests rather than 90, the database is not being reached. diff --git a/cloud/apps/web/.gitignore b/cloud/apps/web/.gitignore index 245259b..d3dd205 100644 --- a/cloud/apps/web/.gitignore +++ b/cloud/apps/web/.gitignore @@ -1,2 +1,8 @@ .vercel .env* + +# Agent-skill docs auto-fetched by `vercel integration add` (Resend usage +# reference for AI assistants). Not application code; safe to re-fetch. +.agents/ +.claude/ +skills-lock.json diff --git a/cloud/apps/web/next.config.ts b/cloud/apps/web/next.config.ts index 045cfbd..320149a 100644 --- a/cloud/apps/web/next.config.ts +++ b/cloud/apps/web/next.config.ts @@ -1,3 +1,7 @@ +// Loads cloud/.env before Next reads anything. Next only looks for .env in its +// own project directory (apps/web), so the workspace-root file the README tells +// people to create was never picked up — see packages/core/src/env.ts. +import "@ghost/core/env"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { diff --git a/cloud/apps/web/package.json b/cloud/apps/web/package.json index 96fdc68..ca5a354 100644 --- a/cloud/apps/web/package.json +++ b/cloud/apps/web/package.json @@ -13,6 +13,7 @@ "test": "vitest run" }, "dependencies": { + "@auth/prisma-adapter": "^2.11.3", "@ghost/core": "workspace:*", "bullmq": "^5.34.4", "clsx": "^2.1.1", diff --git a/cloud/apps/web/src/app/(app)/recordings/page.tsx b/cloud/apps/web/src/app/(app)/recordings/page.tsx index 1236129..27c86f3 100644 --- a/cloud/apps/web/src/app/(app)/recordings/page.tsx +++ b/cloud/apps/web/src/app/(app)/recordings/page.tsx @@ -32,7 +32,7 @@ export default async function RecordingsPage() { return (
@@ -61,8 +61,8 @@ export default async function RecordingsPage() {
@@ -57,18 +57,18 @@ export default async function WorkflowsPage() {
- This deployment has NODE_ENV=production and no OAuth app
- configured, so there is no way to sign in. Set either{" "}
- AUTH_GITHUB_ID/AUTH_GITHUB_SECRET or{" "}
- AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET, with the
- app's callback at{" "}
+ This deployment has NODE_ENV=production and no sign-in method
+ configured. Set AUTH_GITHUB_ID/AUTH_GITHUB_SECRET,{" "}
+ AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET, or{" "}
+ RESEND_API_KEY/RESEND_EMAIL_DOMAIN for email
+ magic links. OAuth apps need their callback at{" "}
https://<this-domain>/api/auth/callback/<github|google>.
See docs/DEPLOY.md.