Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down
24 changes: 17 additions & 7 deletions dev/local/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
services,
} from './services';
import { syncEnvVars } from './env-sync';
import { getWranglerRegistryPath } from './wrangler-registry';
import {
getSessionName,
sessionExists,
Expand Down Expand Up @@ -188,11 +189,13 @@ async function cmdUp(targets: string[], repoRoot: string): Promise<void> {
}

// --- 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<string, string> = { 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<string, string> = {
KILO_PORT_OFFSET: String(portOffset),
WRANGLER_REGISTRY_PATH: wranglerRegistryPath,
};
if (process.env.PORT !== undefined && process.env.PORT !== '') {
sessionEnv.PORT = String(getService('nextjs').port);
}
Expand Down Expand Up @@ -393,7 +396,7 @@ async function cmdUp(targets: string[], repoRoot: string): Promise<void> {
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}`
Expand All @@ -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 };
Expand Down
17 changes: 17 additions & 0 deletions dev/local/wrangler-registry.test.ts
Original file line number Diff line number Diff line change
@@ -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')
);
});
5 changes: 5 additions & 0 deletions dev/local/wrangler-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as path from 'node:path';

export function getWranglerRegistryPath(repoRoot: string): string {
return path.resolve(repoRoot, '.wrangler', 'dev-registry');
}
Loading