Repository navigation
Runnable worktrees: .codev/config.json worktree block + afx dev #689
Description
Issue 1 — Runnable worktrees: .codev/config.json worktree block + afx dev command
Problem
Codev spawns builder worktrees as fresh git checkouts, but doesn't set them up for running. Today only root-level .env and .codev/config.json are symlinked into the worktree (via symlinkConfigFiles in packages/codev/src/agent-farm/commands/spawn-worktree.ts). There is:
- No automatic
pnpm install, sonode_modulesis empty - No per-package env file handling (monorepos with
packages/*/.envleave those unlinked) - No standardized way to spawn the worktree's dev server
- No way to serialize dev environments across main + worktrees (running multiple dev servers simultaneously breaks on CORS, OAuth, cookie scoping, webhook URLs — URLs are load-bearing)
The practical consequence: reviewers cannot easily test a builder's changes. They must manually cd .builders/<name>/, run pnpm install, figure out the right dev command, hope the env is correct, and handle port conflicts themselves. This blocks any protocol that wants a pre-PR review step, and also degrades day-to-day builder inspection for every existing protocol (bugfix, air, spir, aspir).
Proposed solution
Two coupled additions to the builder spawn path:
Part A — Extend .codev/config.json with a worktree block
Add a new top-level worktree section to UserConfig at packages/codev/src/agent-farm/types.ts:
export interface UserConfig {
// ... existing fields (shell, harness, templates, etc.) ...
/** Worktree setup: file symlinks, post-spawn commands, dev command */
worktree?: {
/** Glob patterns of files to symlink from workspace root into each new worktree */
symlinks?: string[];
/** Shell commands to run in each new worktree after `createWorktree` completes */
postSpawn?: string[];
/** How to run the app in the worktree (consumed by `afx dev`) */
devCommand?: string;
};
}Defaults (when the block is omitted or fields are unset) — zero inference, zero behavior change for existing users:
symlinks: effectively unset → existingsymlinkConfigFilesbehavior is preserved (root.env+.codev/config.jsononly). No glob-based expansion; no assumptions about monorepo structure.postSpawn:[](empty). Nothing runs. Codev does NOT try to detectpnpm-lock.yaml/package-lock.json/yarn.lock/Cargo.lock/ etc. and infer an install command — project toolchains are too diverse for that to be safe.devCommand: unset →afx dev <builder>fails with a clear error pointing the user to configure it.
Rationale: Codev supports diverse project stacks (pnpm, npm, yarn, bun, cargo, poetry/uv, go mod, mixed). Any attempt to auto-infer the install or dev command is fragile and surprising when it guesses wrong. Users who want runnable worktrees opt in explicitly for their stack. Users who don't need them see zero behavior change from today.
Implementation:
- Extend
symlinkConfigFilesinpackages/codev/src/agent-farm/commands/spawn-worktree.tsto walk thesymlinksglob list when it is configured. For each match in the main workspace, create a symlink at the same relative path inside the worktree, creating intermediate directories as needed. Whenworktree.symlinksis unset or the block is absent, preserve existing behavior exactly — the hardcoded root.env+.codev/config.jsonsymlinks continue as today, with no additional traversal. - Add a new
runPostSpawnHooks(worktreePath: string, commands: string[]): Promise<void>helper inspawn-worktree.ts. Executes each command synchronously withcwd = worktreePath, using the existingrun()shell utility. Logs progress as "Running post-spawn hook (1/2): pnpm install...". On failure, aborts the spawn with a clear error that includes the failing command and exit code. - Call
runPostSpawnHooksin each spawn function (spawnSpec,spawnTask,spawnProtocol,spawnWorktree,spawnBugfix) immediately aftercreateWorktreereturns. All five sites inpackages/codev/src/agent-farm/commands/spawn.ts. - Add
getWorktreeConfig(workspaceRoot)helper inpackages/codev/src/agent-farm/utils/config.tsthat resolves theworktreeblock with defaults applied.
Part B — New CLI command: afx dev <builder-id>
Register a new command in packages/codev/src/agent-farm/cli.ts and implement in a new module packages/codev/src/agent-farm/commands/dev.ts.
Behavior:
afx dev <builder-id>- Look up the builder and its worktree path. Extract
findBuilderByIdandfindBuilderByIssuefrompackages/codev/src/agent-farm/commands/attach.tsinto a sharedpackages/codev/src/agent-farm/lib/builder-lookup.ts, then have bothattach.tsand the newdev.tsimport from there (avoids duplicating the Tower-fallback logic). - Read
worktree.devCommandfrom.codev/config.json; error with helpful message if unset. - Check for any existing Tower-managed dev PTY (terminals with
type: 'dev'). If found:- Prompt the user:
Currently running dev for <other-builder-id>. Stop it and start <this-builder-id>? [y/N] - On
y: kill the existing dev PTY via Tower's terminal API, then proceed - On
N: abort without spawning a new one
- Prompt the user:
- Create a new Tower-managed PTY via
createPtySession(existing helper inspawn-worktree.ts) with:cwd= the builder's worktree path- Command =
worktree.devCommand type='dev'— a new terminal type. Implementation: extract a namedTerminalTypeunion inpackages/core/src/tower-client.ts('architect' | 'builder' | 'shell' | 'dev'), replace the existing inline unions increateTerminaloptions andcreatePtySessionregistration (the latter currently omits'architect'— fix while extracting), and add'dev'.label="Dev: <builder-id>"— explicit label so the Tower dashboard and the VSCode terminal-bridge name the tab consistently. The Towerterminal-spawnedSSE event must announce this terminal so the VSCode extension's terminal-manager can auto-open it as a tab; verify the SSE bus does not filter bytype(broaden if it does). This is the API surface VSCode sidebar: View Diff + Run/Stop Dev Server commands #690 consumes.- Env inherited from main's workspace (worktree's symlinked env files apply naturally).
- Print the Tower terminal WebSocket URL so the user can attach from the dashboard,
afx attach, or VSCode. - Dev server runs until the user stops it (Ctrl+C after attaching, dashboard Stop button, or
afx dev --stop).
Add --stop flag:
afx dev --stopFinds the current Tower-managed dev PTY (type 'dev') and kills it. No-op if none is running.
Port conflict design — serial swap on main's URLs
The dev PTY uses the same ports and URLs as main by design. No port allocation, no env templating. This preserves URL-dependent mechanisms (CORS allowlists, OAuth callbacks, cookie scoping, CSP connect-src, webhook URLs) that would break if the worktree's dev ran on different ports.
Consequence: only one dev environment can run at a time — either main (started by the user themselves, outside Codev's control) or a Codev-managed builder dev. The swap-detection in afx dev handles builder-to-builder swaps automatically. Main-to-builder swaps require the user to stop main themselves (Codev never kills processes it didn't spawn).
If port is in use when afx dev runs (because main is still active), the spawned dev command will fail at bind time with its own clear error. No special handling needed from Codev.
Files to touch
New
packages/codev/src/agent-farm/commands/dev.ts— implementsafx dev <builder-id>and--stoppackages/codev/src/agent-farm/lib/builder-lookup.ts— extractedfindBuilderById/findBuilderByIssuehelpers (shared byattachanddev)packages/codev/src/agent-farm/__tests__/dev.test.ts— unit tests for builder lookup, devCommand resolution, existing-dev detection,--stopbehavior
Modified
packages/codev/src/agent-farm/types.ts— addworktreeblock toUserConfigpackages/core/src/tower-client.ts— define & exportTerminalType('architect' | 'builder' | 'shell' | 'dev'); use it increateTerminaloptionspackages/codev/src/agent-farm/commands/spawn-worktree.ts— extendsymlinkConfigFiles, addrunPostSpawnHooks, switchcreatePtySessionregistration toTerminalType, setlabel: "Dev: <builder-id>"for dev PTYspackages/codev/src/agent-farm/commands/spawn.ts— callrunPostSpawnHooksafter eachcreateWorktreepackages/codev/src/agent-farm/utils/config.ts— addgetWorktreeConfighelperpackages/codev/src/agent-farm/commands/attach.ts— switch to importingfindBuilderById/findBuilderByIssuefromlib/builder-lookup.tspackages/codev/src/agent-farm/cli.ts— registerafx devcommand- Dashboard / Tower terminal-list code that case-switches on terminal
type— add a'dev'branch (audit during implementation; greptype === 'builder'/type === 'shell'underpackages/codev/src/agent-farm/dashboard/) codev-skeleton/.codev/config.json— add a commented-outworktreeblock as a discoverable hint for new projects spawned bycodev initpackages/codev/src/agent-farm/__tests__/spawn-worktree.test.ts— add cases for glob-symlinking andrunPostSpawnHookssuccess + failureCLAUDE.md/AGENTS.md— document the newworktreeconfig block, theafx devcommand, and include a "Runnable Worktree Recipes" section with ready-to-pasteworktreeblocks for common stacks (pnpm monorepo, npm project, yarn, bun, cargo, poetry/uv, go mod). Reinforce that Codev does NOT auto-infer — users must pick the recipe that matches their stack.
Acceptance criteria
- Existing behavior preserved (backward compatibility is absolute): repositories without a
worktreeblock in.codev/config.jsoncontinue to spawn builders exactly as today. No glob expansion runs, no install runs, no additional env files symlinked. Only the existing hardcoded root.env+.codev/config.jsonsymlinks apply. A user who never opens.codev/config.jsonto add aworktreeblock observes zero behavior change. - Opt-in glob symlinking: when
worktree.symlinksis configured with glob patterns in.codev/config.json, spawning a builder produces symlinks for each matched file from the workspace root into the new worktree at the correct relative path (including nested paths likepackages/foo/.env). - postSpawn runs: after
createWorktree, the configured commands execute in the worktree. Ifpnpm installis the command,node_modules/is populated before the builder session starts. - postSpawn failure aborts spawn: if a postSpawn command exits non-zero, the spawn is aborted with a clear error identifying the failing command. Worktree is not left in a half-setup state.
afx devspawns dev PTY: for any existing builder (bugfix, air, spir, aspir) withworktree.devCommandconfigured,afx dev <builder-id>creates a Tower PTY visible in the dashboard. Dev server output streams to anyone who attaches.afx devswap detection: with dev already running for builder A, runningafx dev <builder-B>prompts the user to stop A before starting B. Confirming swaps; declining aborts.afx dev --stop: stops the current dev PTY; no-op if none running.- Port conflict messaging: when main's dev is running,
afx devfails at bind time with the dev command's own port-in-use error (no Codev intervention needed). - Tests: new unit tests cover
runPostSpawnHooks, glob symlink expansion,afx devbuilder lookup,afx devexisting-dev detection. - Docs updated: CLAUDE.md and AGENTS.md describe the
worktreeblock schema, theafx devcommand, and include a "Runnable Worktree Recipes" section with ready-to-paste configurations for pnpm, npm, yarn, bun, cargo, poetry/uv, and go mod — making clear that Codev does not auto-detect the stack and users must copy the recipe matching their toolchain. - Dev PTY label & SSE: dev terminals are created with
label: "Dev: <builder-id>"so the dashboard and (via VSCode sidebar: View Diff + Run/Stop Dev Server commands #690) VSCode display a meaningful tab name. The Towerterminal-spawnedSSE event includes the newtype: 'dev'value so the VSCode terminal-manager can auto-open the PTY as a tab.
Non-goals (explicitly out of scope)
- Per-worktree port allocation / env templating (would break CORS + URLs; see design rationale)
- Automatic stopping of main's dev when
afx devruns (main is user's responsibility) - Per-worktree database isolation (shared DB acceptable for most bug fixes / small features; users who need isolation add Docker to
postSpawnthemselves) - Auto-detecting the dev command from
package.jsonscripts (explicitworktree.devCommandonly) - Parallel runnable dev environments (serial by design — deploy previews like Vercel/Netlify are the right tool for parallel preview)
Benefits to existing protocols (value delivered without any new protocol)
- Bugfix / AIR builders get properly installed node_modules and full env symlinks — reviewers can run the builder's changes without manual setup
- SPIR / ASPIR builders same — reviewers can test the implementation phase's output in a runnable worktree
- Any builder can have its dev server spawned via a single
afx dev <id>command; previously required manual cd + install + run
Reference implementations / existing patterns
- Builder state lookup pattern: see existing
attachcommand atpackages/codev/src/agent-farm/commands/attach.ts - Tower PTY creation:
createPtySessioninpackages/codev/src/agent-farm/commands/spawn-worktree.ts - Tower terminal listing / killing: see Tower REST API in
packages/core/src/tower-client.ts - Shell command execution with logged failure:
run()inpackages/codev/src/agent-farm/utils/shell.ts - Glob expansion: use Node's built-in
fs.globSync(Node 20+)
Dependencies
- None. This issue is standalone and foundational.
Blocks
- VSCode sidebar: View Diff + Run/Stop Dev Server commands #690 (VSCode sidebar: View Diff + Run/Stop Dev Server commands) depends on the
worktree.devCommandconcept, the'dev'terminal type, the"Dev: <builder-id>"label, and the SSE announcement from this issue. - PIR protocol: plan-review + code-review gates for issue-driven work #691 (PIR protocol) depends on this for the code-review gate's testability.
Activity
Merge pull request #731 from cluesmith/feat/689-690-runnable-worktree…
refactor(core): extract TerminalType and add 'dev'
feat(agent-farm/config): add worktree block to UserConfig + getWorktr…
feat(agent-farm/spawn): glob symlinks + post-spawn hooks
refactor(agent-farm): extract builder-lookup, reuse resolveAgentName
feat(agent-farm/cli): afx dev <builder-id> + --stop
docs(claude/agents): runnable worktrees section + recipes
feat(vscode): codev.runWorktreeDev + codev.stopWorktreeDev
feat: run worktree.postSpawn for an existing builder
refactor(postspawn): stream output live + single source of truth
feat(setup): apply worktree.symlinks too, not just postSpawn