Skip to content

feat: proj:<name> spawn prefix (spawn in a SPAWN_CWD subdirectory)#60

Draft
kwliang1 wants to merge 1 commit into
sf8193:mainfrom
kwliang1:feat/proj-spawn-cwd
Draft

feat: proj:<name> spawn prefix (spawn in a SPAWN_CWD subdirectory)#60
kwliang1 wants to merge 1 commit into
sf8193:mainfrom
kwliang1:feat/proj-spawn-cwd

Conversation

@kwliang1

Copy link
Copy Markdown
Collaborator

What

Adds a proj:<name> (alias dir:<name>) topic prefix to doSpawnSession that spawns the session directly inside an existing subdirectory of SPAWN_CWD — the real checkout, no worktree. Mirrors the existing worktree:/wt: prefix parsing.

proj:car-ship-prototype <your task>

Why

Per-project tooling that keys off the session cwd currently can't distinguish projects, because every plain spawn shares the single SPAWN_CWD. Concretely: claude-mem shards memory by git-repo-root / directory — but with all sessions launching at SPAWN_CWD it labels everything one project and pools it.

proj:<name> lets each project's sessions launch in their own directory, so such tooling shards per project, without the ephemeral-branch / cleanup overhead of worktree: (which is great for isolated build tasks but deletes its branch on exit — wrong fit for normal interactive work on the real checkout).

How

  • Parse ^(?:proj|dir):(\S+)\s+ from the topic (same pattern as worktree:), strip it so it doesn't leak into thread names/prompts.
  • If set (and not a worktree spawn), resolve effectiveCwd = resolve(SPAWN_CWD, name), erroring if the dir doesn't exist. existsSync is already imported.
  • Worktree path is unchanged; proj: and worktree: are mutually exclusive (only one matches ^).

Testing

  • bun build daemon.ts --target=bun
  • bun build bridge.ts --target=bun

Notes

Draft for review. No change to default behavior — plain spawns still land at SPAWN_CWD.

🤖 Generated with Claude Code

…directory)

Adds a `proj:<name>` (alias `dir:<name>`) topic prefix that spawns a session
directly inside an existing subdirectory of SPAWN_CWD — the real checkout, no
worktree. Mirrors the existing `worktree:` prefix parsing.

Motivation: per-project tooling that keys off the session cwd (e.g. claude-mem,
which shards memory by git-repo-root / directory) currently pools everything
under SPAWN_CWD because every plain spawn shares that one cwd. `proj:<name>`
lets each project's sessions launch in their own directory so such tooling
shards per project, without the ephemeral-branch overhead of `worktree:`.

Usage: `proj:car-ship-prototype <your task>`

Compile check: `bun build daemon.ts` / `bun build bridge.ts` both pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sf8193 sf8193 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #60 Review — proj:<name> spawn prefix

Reviewed by two specialist agents (sp-reviewer: architecture, typescript-reviewer: correctness). Findings merged and deduplicated below.

Not in diff but noted: The spawn_session tool schema in daemon/bridge-dispatch.ts has no proj parameter — agent sessions can't discover or use this feature via the tool contract. The router also has no spawn-proj: shorthand (unlike spawn-wt: for worktrees). Both make proj: invisible to callers without reading source.

let effectiveCwd = spawnCwd
if (projTarget) {
// Spawn in an existing subdirectory of SPAWN_CWD (real checkout, no worktree).
const projDir = resolve(spawnCwd, projTarget)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — path traversal (flagged by both reviewers)

resolve(spawnCwd, projTarget) will happily resolve ../../etc or an absolute path. There is no guard that projDir stays inside spawnCwd. A user with Discord spawn access can set effectiveCwd to any filesystem path.

Fix:

if (!projDir.startsWith(spawnCwd + '/')) {
  throw new Error(`proj target "${projTarget}" escapes SPAWN_CWD`)
}

let worktreeRepo: string | undefined
let worktreePath: string | undefined
let effectiveCwd = spawnCwd
if (projTarget) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix — no mutual exclusion with worktreeTarget (flagged by both reviewers)

worktree:foo proj:bar topic will set both targets. The projTarget block runs first and sets effectiveCwd, then the worktreeTarget block silently overwrites it. The proj directive is discarded without error.

Fix: add a guard after both parsers:

if (projTarget && worktreeTarget) {
  throw new Error('proj:/dir: and worktree:/wt: prefixes cannot be combined')
}

// subdirectory of SPAWN_CWD (no worktree), so per-project tooling (e.g. claude-mem)
// shards memory/state by that directory instead of pooling everything under SPAWN_CWD.
let projTarget: string | undefined
const projMatch = topic.match(/^(?:proj|dir):(\S+)\s+/)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix — resume/respawn loses proj CWD (flagged by both reviewers)

The proj: prefix is stripped from topic here. SessionInfo stores the stripped topic but never records projTarget or projDir. When tryResume/tryRespawn reconstruct a session from dead.topic, the proj-pinned CWD is lost — the session respawns under spawnCwd instead of projDir.

Fix: persist projDir (or projTarget) in the registry entry alongside worktreeRepo/worktreePath, and pass it through the respawn path.


// Parse proj:<name> / dir:<name> prefix -- spawn directly inside an existing
// subdirectory of SPAWN_CWD (no worktree), so per-project tooling (e.g. claude-mem)
// shards memory/state by that directory instead of pooling everything under SPAWN_CWD.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix — bare proj:name silently falls through

The regex /^(?:proj|dir):(\S+)\s+/ requires trailing whitespace. A bare proj:myapp (no trailing text) won't match — the prefix becomes part of the topic/prompt with no error. The worktree regex has the same pattern, but this is a new instance worth fixing.

Fix: /^(?:proj|dir):(\S+)(?:\s+|$)/ and handle the end-of-string case when slicing.

throw new Error(`proj target "${projTarget}" does not exist at ${projDir}`)
}
effectiveCwd = projDir
process.stderr.write(`daemon: spawning in project dir ${projDir}\n`)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — log line missing session name

All other spawn-path log lines include tmuxName for grepability (e.g. daemon: spawn ${tmuxName}: ...). This line omits it. tmuxName is already assigned by this point (line 166).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants