diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b17f0bd6a2..ccaeda33b2 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -415,7 +415,7 @@ If `CF_AE_TOKEN` is missing, Grafana will still boot — only dashboard queries ### Limitations in local dev -- **Service bindings** between workers don't function in local `wrangler dev`. This affects chains like session-ingest → o11y, webhook-agent → cloud-agent, and app-builder → db-proxy/git-token-service. +- **Service bindings** resolve locally for Workers launched together by `pnpm dev:start` when the bound target is running. Bindings to optional services remain unavailable unless their owning group is started (for example, session-ingest -> o11y requires the `observability` group). - **Webhook → KiloClaw Chat** triggers require the KiloClaw worker running on port 8795. The webhook worker calls it via `KILOCLAW_API_URL` (HTTP, not a service binding) to deliver messages to Stream Chat. Stream Chat credentials (`STREAM_CHAT_API_KEY`, `STREAM_CHAT_API_SECRET`) must be in `kiloclaw/.dev.vars`. - **Cloudflare Containers** (used by cloud-agent, cloud-agent-next, app-builder) always run on Cloudflare's remote infrastructure, even in dev mode. Purely local execution is not possible. - **Analytics Engine writes** are no-ops in `wrangler dev` — there is no local AE simulator. Reads against the real prod datasets still work via the local Grafana above. **Pipelines** and **dispatch namespaces** don't work locally. @@ -436,9 +436,11 @@ export KILO_PORT_OFFSET=auto export KILO_PORT_OFFSET=100 ``` -With `auto`, the primary worktree gets offset 0 (default ports), and secondary worktrees get a deterministic offset based on the directory name. The offset is added to the Next.js port (3000), all worker dev ports, and the URLs generated by `pnpm dev:env`. +With `auto`, the primary worktree gets offset 0 (default ports), and secondary worktrees get a deterministic offset based on the directory name. The offset is added to the Next.js port (3000), all worker dev ports, and the URLs generated by `pnpm dev:env`. Use the same offset when syncing env values and starting or restarting services in a worktree. -Infrastructure containers (`postgres` on 5432, `redis` on 6379, `grafana` on 4000) always bind to their fixed host ports regardless of the offset — they are single shared services, not per-worktree. Only one worktree can run the infra stack at a time; secondary worktrees should either reuse the primary worktree's infra or run `pnpm dev:stop` before starting the infra in another worktree. +`pnpm dev:start` also passes a worktree-local Wrangler service-discovery registry at `.wrangler/dev-registry` into its tmux session. For worktrees with distinct `kilo-dev-*` session names, this allows concurrent offset Worker stacks such as `agents` to use the same local Worker names without resolving bindings to Workers running from sibling worktrees. The absolute registry path is recorded in `dev/logs/manifest.json` for diagnostics. + +Infrastructure containers (`postgres` on 5432, `redis` on 6379, `grafana` on 4000) always bind to their fixed host ports regardless of the offset - they are shared services, not per-worktree instances. Concurrent worktrees reuse those containers, and `pnpm dev:stop` leaves them running while another `kilo-dev-*` session remains active. ## Troubleshooting diff --git a/dev/local/cli.ts b/dev/local/cli.ts index 9db4561bfe..6dad97e1dd 100644 --- a/dev/local/cli.ts +++ b/dev/local/cli.ts @@ -13,6 +13,7 @@ import { services, } from './services'; import { syncEnvVars } from './env-sync'; +import { getWranglerRegistryPath } from './wrangler-registry'; import { getSessionName, sessionExists, @@ -188,11 +189,13 @@ async function cmdUp(targets: string[], repoRoot: string): Promise { } // --- Create tmux session --- - // Pass critical port env into the session so panes see it even when an - // existing tmux server (from a sibling worktree) is running with different - // values. Without this, new windows inherit the server env, not ours, and - // services can bind to the wrong ports. - const sessionEnv: Record = { KILO_PORT_OFFSET: String(portOffset) }; + // Pass critical runtime env into the session so panes see this worktree's + // values even when an existing tmux server is shared with sibling worktrees. + const wranglerRegistryPath = getWranglerRegistryPath(repoRoot); + const sessionEnv: Record = { + KILO_PORT_OFFSET: String(portOffset), + WRANGLER_REGISTRY_PATH: wranglerRegistryPath, + }; if (process.env.PORT !== undefined && process.env.PORT !== '') { sessionEnv.PORT = String(getService('nextjs').port); } @@ -393,7 +396,7 @@ async function cmdUp(targets: string[], repoRoot: string): Promise { selectWindow(sessionName, 0); // --- Write manifest for agents --- - writeManifest(repoRoot, sessionName, startedServices); + writeManifest(repoRoot, sessionName, wranglerRegistryPath, startedServices); console.log( `${GREEN}Started ${startedServices.length} services in session ${sessionName}${RESET}` @@ -420,13 +423,20 @@ type ManifestEntry = { type Manifest = { session: string; portOffset: number; + wranglerRegistryPath: string; services: ManifestEntry[]; }; -function writeManifest(repoRoot: string, sessionName: string, serviceNames: string[]): void { +function writeManifest( + repoRoot: string, + sessionName: string, + wranglerRegistryPath: string, + serviceNames: string[] +): void { const manifest: Manifest = { session: sessionName, portOffset, + wranglerRegistryPath, services: serviceNames.map(name => { const svc = getService(name); return { name, port: svc.port, group: svc.group, type: svc.type }; diff --git a/dev/local/wrangler-registry.test.ts b/dev/local/wrangler-registry.test.ts new file mode 100644 index 0000000000..33f7e46cd1 --- /dev/null +++ b/dev/local/wrangler-registry.test.ts @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { getWranglerRegistryPath } from './wrangler-registry'; + +test('keeps the Wrangler registry beneath the current worktree', () => { + assert.equal( + getWranglerRegistryPath('/tmp/worktrees/feature-a'), + '/tmp/worktrees/feature-a/.wrangler/dev-registry' + ); +}); + +test('gives equal-basename worktrees distinct Wrangler registries', () => { + assert.notEqual( + getWranglerRegistryPath('/tmp/worktrees-a/cloud'), + getWranglerRegistryPath('/tmp/worktrees-b/cloud') + ); +}); diff --git a/dev/local/wrangler-registry.ts b/dev/local/wrangler-registry.ts new file mode 100644 index 0000000000..963cf2af46 --- /dev/null +++ b/dev/local/wrangler-registry.ts @@ -0,0 +1,5 @@ +import * as path from 'node:path'; + +export function getWranglerRegistryPath(repoRoot: string): string { + return path.resolve(repoRoot, '.wrangler', 'dev-registry'); +}