diff --git a/.config/wt.toml b/.config/wt.toml index 0446ad3d..cf2ba032 100644 --- a/.config/wt.toml +++ b/.config/wt.toml @@ -8,13 +8,13 @@ env = "bun run setup:worktree -- --quiet" deps = "wt step copy-ignored || true; test -x node_modules/.bin/vite || bun install" [post-start] -dev = "bun scripts/dev-worktree.ts --detach --port {{ (repo ~ '-' ~ branch) | hash_port }}" +dev = "bun scripts/dev-worktree.ts --detach --seed --port {{ (repo ~ '-' ~ branch) | hash_port }}" [pre-remove] dev = "if test -f .codex/runtime/dev-worktree.pid; then pid=$(cat .codex/runtime/dev-worktree.pid); kill -TERM -$pid 2>/dev/null || kill $pid 2>/dev/null || true; rm -f .codex/runtime/dev-worktree.pid; fi" [aliases] -dev = "wt --yes hook pre-start && bun scripts/dev-worktree.ts --detach --port {{ (repo ~ '-' ~ branch) | hash_port }}" +dev = "wt --yes hook pre-start && bun scripts/dev-worktree.ts --detach --seed --port {{ (repo ~ '-' ~ branch) | hash_port }}" setup = "wt --yes hook pre-start" stop = "if test -f .codex/runtime/dev-worktree.pid; then pid=$(cat .codex/runtime/dev-worktree.pid); kill -TERM -$pid 2>/dev/null || kill $pid 2>/dev/null || true; rm -f .codex/runtime/dev-worktree.pid; fi" url = "echo http://127.0.0.1:{{ (repo ~ '-' ~ branch) | hash_port }}" diff --git a/AGENTS.md b/AGENTS.md index 3912e742..fe69d0dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,8 +24,8 @@ Keep this section as the command map agents normally need, not a full `package.j - `bunx convex dev --typecheck=disable` — local Convex backend/function watcher for manual setup. - `bunx convex codegen` — regenerate `convex/_generated` after Convex API/schema changes. - `bun run setup:worktree` — link `.env.local` and `.convex` from a usable source worktree into the current worktree. Use `-- --from ` or `CLAWHUB_WORKTREE_SOURCE=` when auto-discovery picks the wrong source. -- `bun run dev:worktree` — Worktrunk-managed detached worktree server. Requires `wt` on `PATH`; from that worktree use `wt --yes url` to print the branch URL and `wt --yes stop` to stop it. -- `bun run seed:dev` — canonical local seed path; runs worktree setup, waits for local Convex, seeds local fixtures plus the public corpus, and refreshes stats. +- `bun run dev:worktree` — Worktrunk-managed detached worktree server that also seeds local fixtures plus the public corpus once before starting the app when `VITE_CONVEX_URL` and `CONVEX_DEPLOYMENT` are local. Requires `wt` on `PATH`; from that worktree use `wt --yes url` to print the branch URL and `wt --yes stop` to stop it. +- `bun run seed:dev` — manual reseed path; runs worktree setup, waits for local Convex, seeds local fixtures plus the public corpus, and refreshes stats. - `bun run build` — production build (Vite + Nitro). - `bun run ci:static` — required pre-handoff static gate: peer checks, audit, formatting, lint, and dead-code checks. - `bun run ci:unit` — Vitest coverage gate; required for source/test PRs unless docs/config-only. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d83961b7..dbd7b9da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,7 @@ bun run setup:worktree -- --from /path/to/source/worktree CLAWHUB_WORKTREE_SOURCE=/path/to/source/worktree bun run setup:worktree ``` -`dev:worktree` is the Worktrunk entrypoint. It runs the hooks in `.config/wt.toml`, copies ignored dependencies listed in `.worktreeinclude` when possible, falls back to `bun install` if Vite is missing, and starts detached services on a branch-hashed loopback port. Use `wt --yes url` from the same worktree to print the URL. +`dev:worktree` is the Worktrunk entrypoint. It runs the hooks in `.config/wt.toml`, copies ignored dependencies listed in `.worktreeinclude` when possible, falls back to `bun install` if Vite is missing, seeds local fixtures plus the public corpus once when `VITE_CONVEX_URL` and `CONVEX_DEPLOYMENT` are local, refreshes cached global stats, and starts detached services on a branch-hashed loopback port. Use `wt --yes url` from the same worktree to print the URL. The detached server writes runtime state under `.codex/runtime/`. Stop it with `wt --yes stop` before removing the worktree. @@ -121,9 +121,9 @@ one. Without those workers, local ClawScan and Skill Card jobs stay pending until you opt in, seed/mock results, or use the production workflows. -### Seed the database +### Reseed the database -Populate local QA fixtures and the committed public corpus so the UI isn't empty: +`dev:worktree` seeds local QA fixtures and the committed public corpus before starting the app when `VITE_CONVEX_URL` points at local Convex and `CONVEX_DEPLOYMENT` is an anonymous/local deployment marker, then records `.codex/runtime/dev-worktree.seeded` so ordinary restarts skip the expensive corpus pass. Remote-backed previews or mismatched deployment markers skip seeding and keep starting. To force the seed path without restarting the preview: ```bash bun run seed:dev diff --git a/scripts/dev-worktree.test.ts b/scripts/dev-worktree.test.ts index 0edabc8d..3c39e3e6 100644 --- a/scripts/dev-worktree.test.ts +++ b/scripts/dev-worktree.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { applyLocalDevWorkerToken, @@ -12,10 +13,22 @@ import { isRunningPid, parseArgs, parseEnv, + shouldSeedLocalData, shouldStartDevWorkers, } from "./dev-worktree"; describe("dev-worktree helpers", () => { + it("seeds local fixtures and public corpus when Worktrunk starts dev services", () => { + const config = readFileSync(".config/wt.toml", "utf8"); + + expect(config).toContain( + '[post-start]\ndev = "bun scripts/dev-worktree.ts --detach --seed --port', + ); + expect(config).toContain( + '[aliases]\ndev = "wt --yes hook pre-start && bun scripts/dev-worktree.ts --detach --seed --port', + ); + }); + it("parses env files without treating inline comments as values", () => { expect( parseEnv(` @@ -165,6 +178,106 @@ describe("dev-worktree helpers", () => { }); }); + it("seeds local data only for local Convex URLs", () => { + expect( + shouldSeedLocalData( + { seed: true, seedOnly: false }, + "http://127.0.0.1:3210", + "anonymous:anonymous-agent", + ), + ).toEqual({ + seed: true, + fatal: false, + reason: null, + }); + + expect( + shouldSeedLocalData( + { seed: false, seedOnly: false }, + "http://127.0.0.1:3210", + "anonymous:anonymous-agent", + ), + ).toEqual({ + seed: false, + fatal: false, + reason: "--seed was not passed", + }); + + expect( + shouldSeedLocalData( + { seed: true, seedOnly: false }, + "https://example.convex.cloud", + "prod:shared-deployment", + ), + ).toEqual({ + seed: false, + fatal: false, + reason: "VITE_CONVEX_URL is not local", + }); + + expect( + shouldSeedLocalData( + { seed: true, seedOnly: true }, + "https://example.convex.cloud", + "prod:shared-deployment", + ), + ).toEqual({ + seed: false, + fatal: true, + reason: "VITE_CONVEX_URL is not local", + }); + + expect( + shouldSeedLocalData( + { seed: true, seedOnly: false }, + "http://127.0.0.1:3210", + "prod:shared-deployment", + ), + ).toEqual({ + seed: false, + fatal: false, + reason: "CONVEX_DEPLOYMENT is missing or not local", + }); + + expect( + shouldSeedLocalData( + { seed: true, seedOnly: true }, + "http://127.0.0.1:3210", + "prod:shared-deployment", + ), + ).toEqual({ + seed: false, + fatal: true, + reason: "CONVEX_DEPLOYMENT is missing or not local", + }); + + expect( + shouldSeedLocalData( + { seed: true, seedOnly: false }, + "http://127.0.0.1:3210", + "anonymous:anonymous-agent", + "anonymous:anonymous-agent\nhttp://127.0.0.1:3210", + ), + ).toEqual({ + seed: false, + fatal: false, + reason: "local seed already completed for this Convex deployment", + }); + + expect( + shouldSeedLocalData( + { seed: true, seedOnly: true }, + "http://127.0.0.1:3210", + "anonymous:anonymous-agent", + "anonymous:anonymous-agent\nhttp://127.0.0.1:3210", + ), + ).toEqual({ + seed: true, + fatal: false, + reason: null, + }); + }); + it("does not start dev workers by default for non-local Convex URLs", () => { expect(shouldStartDevWorkers({ workers: true }, "https://example.convex.cloud")).toEqual({ start: false, diff --git a/scripts/dev-worktree.ts b/scripts/dev-worktree.ts index 4322dc7e..eca018b3 100644 --- a/scripts/dev-worktree.ts +++ b/scripts/dev-worktree.ts @@ -21,6 +21,7 @@ const REACHABILITY_POLL_MS = 500; const RUNTIME_DIR = ".codex/runtime"; const DETACHED_PID_FILE = `${RUNTIME_DIR}/dev-worktree.pid`; const DETACHED_LOG_FILE = `${RUNTIME_DIR}/dev-worktree.log`; +const SEED_SENTINEL_FILE = `${RUNTIME_DIR}/dev-worktree.seeded`; const LOCAL_DEV_WORKER_TOKEN = "local-dev-worker-token"; const managedChildren = new Set(); @@ -81,6 +82,36 @@ export function shouldStartDevWorkers(options: Pick, convexU return { start: true, reason: null }; } +export function shouldSeedLocalData( + options: Pick, + convexUrl: string, + convexDeployment: string | null | undefined, + completedSeedTarget: string | null = null, +) { + if (!options.seed) return { seed: false, fatal: false, reason: "--seed was not passed" }; + if (!isLocalConvexUrl(convexUrl)) { + return { seed: false, fatal: options.seedOnly, reason: "VITE_CONVEX_URL is not local" }; + } + if (!isLocalConvexDeployment(convexDeployment)) { + return { + seed: false, + fatal: options.seedOnly, + reason: "CONVEX_DEPLOYMENT is missing or not local", + }; + } + if ( + !options.seedOnly && + completedSeedTarget === buildSeedTarget(convexUrl, convexDeployment.trim()) + ) { + return { + seed: false, + fatal: false, + reason: "local seed already completed for this Convex deployment", + }; + } + return { seed: true, fatal: false, reason: null }; +} + export function applyLocalDevWorkerToken(env: NodeJS.ProcessEnv) { env.SECURITY_SCAN_WORKER_TOKEN = LOCAL_DEV_WORKER_TOKEN; return LOCAL_DEV_WORKER_TOKEN; @@ -145,6 +176,20 @@ export function isLocalConvexUrl(value: string) { } } +export function isLocalConvexDeployment(value: string | null | undefined) { + const deployment = value?.trim(); + return ( + deployment === "anonymous-agent" || + deployment?.startsWith("anonymous:") || + deployment?.startsWith("local:") || + false + ); +} + +function buildSeedTarget(convexUrl: string, convexDeployment: string) { + return `${convexDeployment}\n${convexUrl}`; +} + function readDetachedPid() { if (!existsSync(DETACHED_PID_FILE)) return null; const raw = readFileSync(DETACHED_PID_FILE, "utf8").trim(); @@ -153,6 +198,19 @@ function readDetachedPid() { return Number.isSafeInteger(pid) && pid > 0 ? pid : null; } +function readSeedSentinel() { + try { + return readFileSync(SEED_SENTINEL_FILE, "utf8").trim() || null; + } catch { + return null; + } +} + +function writeSeedSentinel(convexUrl: string, convexDeployment: string) { + mkdirSync(RUNTIME_DIR, { recursive: true }); + writeFileSync(SEED_SENTINEL_FILE, `${buildSeedTarget(convexUrl, convexDeployment)}\n`); +} + export function isRunningPid(pid: number | null) { if (pid === null || !Number.isSafeInteger(pid) || pid <= 0) return false; try { @@ -475,11 +533,23 @@ async function main() { process.exit(1); } + const convexDeployment = process.env.CONVEX_DEPLOYMENT; + const seed = shouldSeedLocalData(options, convexUrl, convexDeployment, readSeedSentinel()); + if (options.seed && !seed.seed) { + const message = `Skipping local fixtures and public corpus seed: ${seed.reason}.`; + if (seed.fatal) { + console.error(message); + console.error("Manual dev seeding only runs when VITE_CONVEX_URL points at localhost."); + process.exit(1); + } + console.warn(`${message} Continuing without seeding.`); + } + applyLocalConvexEnvForUrl(process.env, convexUrl); await ensureConvex(convexUrl); await configureLocalConvexEnv(convexUrl); - if (options.seed) { + if (seed.seed) { console.log("Seeding local fixtures and public corpus..."); const seedStatus = await runConvexFunctionWhenReady([ "convex", @@ -499,6 +569,7 @@ async function main() { "statsMaintenance:updateGlobalStatsAction", ]); if (statsStatus !== 0) exitAfterStoppingManagedChildren(statsStatus); + writeSeedSentinel(convexUrl, convexDeployment?.trim() ?? ""); } if (options.seedOnly) { diff --git a/specs/dev-seeding.md b/specs/dev-seeding.md index 494e6221..6d537383 100644 --- a/specs/dev-seeding.md +++ b/specs/dev-seeding.md @@ -10,9 +10,14 @@ read_when: Local fixture seeding is command-driven by default: -- CLI seeding (`bun run seed:dev`) populates shared catalog fixtures under `@local`, including - skill, plugin, scanner, and moderation fixtures. It also imports the committed public corpus and - refreshes cached global stats. This is the documented local setup path. +- Worktree dev startup (`bun run dev:worktree`) seeds shared catalog fixtures under `@local`, + including skill, plugin, scanner, and moderation fixtures, before starting the local app when + `VITE_CONVEX_URL` points at local Convex and `CONVEX_DEPLOYMENT` is an anonymous/local deployment + marker. It also imports the committed public corpus, refreshes cached global stats, and writes + `.codex/runtime/dev-worktree.seeded` so routine restarts skip the expensive corpus pass. This is + the documented first-run local setup path. +- CLI seeding (`bun run seed:dev`) runs the same seed path manually without starting the preview and + bypasses the first-run sentinel. - `bun run seed:public-corpus` is the lower-level corpus-only import command. Use it for corpus fixture work, not as the default local setup command. - `bun run validate:public-corpus` validates the committed public corpus fixture without seeding. diff --git a/specs/dev-worktrees.md b/specs/dev-worktrees.md index b4317d52..8a3ea321 100644 --- a/specs/dev-worktrees.md +++ b/specs/dev-worktrees.md @@ -58,10 +58,11 @@ The copy step is best effort. If `.convex` is already a symlink to the source wo ## Runtime Contract -`scripts/dev-worktree.ts` loads `.env.local`, checks `VITE_CONVEX_URL`, starts local Convex if it is not reachable, then starts Vite on the requested port. Detached runtime state lives under `.codex/runtime/`: +`scripts/dev-worktree.ts` loads `.env.local`, checks `VITE_CONVEX_URL`, starts local Convex if it is not reachable, optionally seeds local fixtures plus the public corpus once when both `VITE_CONVEX_URL` and `CONVEX_DEPLOYMENT` describe a local target, then starts Vite on the requested port. Worktrunk passes `--seed` for normal `dev:worktree` startup. Detached runtime state lives under `.codex/runtime/`: - `.codex/runtime/dev-worktree.pid` - `.codex/runtime/dev-worktree.log` +- `.codex/runtime/dev-worktree.seeded` Use `wt --yes stop` before removing or recreating a worktree. If a stale pid blocks startup, stop the service and inspect the runtime log before deleting files by hand. @@ -75,4 +76,4 @@ operator provides `CODEX_HOME`. ## Seeding Contract -`bun run seed:dev` uses the same worktree setup helper and the same local Convex readiness checks as the detached dev server. It must remain the documented default seed command. Lower-level Convex calls and `seed:public-corpus` are recovery or fixture-authoring tools, not the first-run path. +`bun run dev:worktree` is the documented first-run path and seeds before starting the detached app when both the Convex URL and deployment marker are local. Successful automatic seeding records the Convex deployment plus URL in `.codex/runtime/dev-worktree.seeded`, so restarts against the same local backend skip seeding. Remote-backed previews or mismatched deployment markers skip seeding and keep starting. `bun run seed:dev` uses the same worktree setup helper and the same local Convex readiness checks as the detached dev server for manual reseeding, bypasses the sentinel, and remains local-only. Lower-level Convex calls and `seed:public-corpus` are recovery or fixture-authoring tools, not the first-run path.