feat: Nexus-owned git worktrees with session-as-branch naming (closes #10) #13

Merged
lz merged 14 commits from worktree-issue-10-nexus-worktrees into main 2026-05-15 22:40:27 +02:00
Owner

Closes #10.

Summary

Nexus takes over git worktree lifecycle so the session name becomes the git branch name directly — no hardcoded worktree- prefix.

A session named auth-rewrite now lives at /workspace/.nexus/worktrees/auth-rewrite on a branch called auth-rewrite. claude --remote-control is launched with cwd set to that worktree (no --worktree flag), which sidesteps claude-code's prefix limitation entirely.

What changed

  • createSession now: validates the session name against git check-ref-format --branch rules → pre-creates the worktree with git worktree add -b <name> → applies .worktreeinclude (best-effort) → seeds workspace trust for the new path → launches the tmux window. Retry-once cleanup if git worktree add hits stale state. Symmetric catch-block teardown on any failure.
  • deleteSession mirrors creation: tmux kill-windowgit worktree remove --forcegit branch -D. All best-effort.
  • buildLaunchCommand drops --worktree, cds into the Nexus worktree, and tags the Remote Control identifier as <workspace-label> · <session-name> so claude.ai/code disambiguates sessions across workspaces.
  • apply-worktreeinclude.sh (new, in worker/) reimplements the gitignored-file copy contract using git ls-files --others --ignored --exclude-from=....
  • Entrypoint appends .nexus/ to /workspace/.gitignore idempotently on boot.
  • AGENTS.md updated: facts #1, #3, #4, #7, #9 + a new "Session worktrees" subsection.

Two helpers extracted

  • lib/git-ref.tsvalidateGitRefName() (26 tests, table-driven)
  • lib/docker-exec.tsexecAndCapture() demuxes dockerode's framed stdout/stderr; throws on poll-timeout rather than silently returning exit 0

What stays unchanged

  • DB schema (no migration). Legacy worktree-* rows keep their existing branches until naturally deleted.
  • Nexus's web UI session labels (still the bare session name).
  • The tmux window name, the worktree dir name, the DB branch/worktree_name columns — all the bare session name (git-ref safe).
  • Only the Remote Control identifier gets the <workspace> · <session> prefix.

Test plan

  • 147 unit tests pass (pnpm test); typecheck clean (pnpm exec tsc --noEmit).
  • Stack rebuilt and running locally — webapp serves at :3001, migrations clean.
  • Smoke test: create workspace → create session auth-rewrite → verify git branch shows bare name, /workspace/.nexus/worktrees/auth-rewrite exists, claude.ai/code shows <workspace> · auth-rewrite.
  • Smoke test: session with slash name (feature/login).
  • Smoke test: illegal name (bad:name) returns 400.
  • Smoke test: delete session — branch + worktree both cleaned up.
  • Smoke test: stale-state recovery — leave junk at .nexus/worktrees/<name>, re-create with same name, verify it cleans up and succeeds.

Commits

13 commits, each focused. Two were review-driven fixes for issues caught in-loop (regex hardening in git-ref.ts, stdout fallback on createSession throws).

Closes #10. ## Summary Nexus takes over git worktree lifecycle so the session name becomes the git branch name directly — no hardcoded `worktree-` prefix. A session named `auth-rewrite` now lives at `/workspace/.nexus/worktrees/auth-rewrite` on a branch called `auth-rewrite`. `claude --remote-control` is launched with cwd set to that worktree (no `--worktree` flag), which sidesteps claude-code's prefix limitation entirely. ## What changed - **`createSession`** now: validates the session name against `git check-ref-format --branch` rules → pre-creates the worktree with `git worktree add -b <name>` → applies `.worktreeinclude` (best-effort) → seeds workspace trust for the new path → launches the tmux window. Retry-once cleanup if `git worktree add` hits stale state. Symmetric catch-block teardown on any failure. - **`deleteSession`** mirrors creation: `tmux kill-window` → `git worktree remove --force` → `git branch -D`. All best-effort. - **`buildLaunchCommand`** drops `--worktree`, `cd`s into the Nexus worktree, and tags the Remote Control identifier as `<workspace-label> · <session-name>` so claude.ai/code disambiguates sessions across workspaces. - **`apply-worktreeinclude.sh`** (new, in `worker/`) reimplements the gitignored-file copy contract using `git ls-files --others --ignored --exclude-from=...`. - **Entrypoint** appends `.nexus/` to `/workspace/.gitignore` idempotently on boot. - **AGENTS.md** updated: facts #1, #3, #4, #7, #9 + a new "Session worktrees" subsection. ## Two helpers extracted - `lib/git-ref.ts` — `validateGitRefName()` (26 tests, table-driven) - `lib/docker-exec.ts` — `execAndCapture()` demuxes dockerode's framed stdout/stderr; throws on poll-timeout rather than silently returning exit 0 ## What stays unchanged - DB schema (no migration). Legacy `worktree-*` rows keep their existing branches until naturally deleted. - Nexus's web UI session labels (still the bare session name). - The tmux window name, the worktree dir name, the DB `branch`/`worktree_name` columns — all the bare session name (git-ref safe). - Only the Remote Control identifier gets the `<workspace> · <session>` prefix. ## Test plan - [x] 147 unit tests pass (`pnpm test`); typecheck clean (`pnpm exec tsc --noEmit`). - [x] Stack rebuilt and running locally — webapp serves at :3001, migrations clean. - [x] Smoke test: create workspace → create session `auth-rewrite` → verify `git branch` shows bare name, `/workspace/.nexus/worktrees/auth-rewrite` exists, claude.ai/code shows `<workspace> · auth-rewrite`. - [x] Smoke test: session with slash name (`feature/login`). - [ ] Smoke test: illegal name (`bad:name`) returns 400. - [ ] Smoke test: delete session — branch + worktree both cleaned up. - [ ] Smoke test: stale-state recovery — leave junk at `.nexus/worktrees/<name>`, re-create with same name, verify it cleans up and succeeds. ## Commits 13 commits, each focused. Two were review-driven fixes for issues caught in-loop (regex hardening in `git-ref.ts`, stdout fallback on createSession throws).
lz added 13 commits 2026-05-15 20:01:02 +02:00
If the exec inspect loop exhausts all 20 polls with Running:true, throw
instead of returning exitCode=0, which would falsely indicate success.
Adds a test that asserts the rejection when inspect always returns Running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
buildLaunchCommand now cds into /workspace/.nexus/worktrees/<name> and
invokes claude --remote-control NAME without --worktree, decoupling the
branch name from claude-code's hardcoded worktree-<name> prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds worker/scripts/apply-worktreeinclude.sh which replicates the
.worktreeinclude copy contract (gitignored files matching include patterns
are copied into the new worktree). Wires the script into the worker image
via Dockerfile COPY + chmod so it is available at
/usr/local/bin/apply-worktreeinclude.sh for invocation from createSession.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(sessions): tag Remote Control identifier with workspace label
Some checks failed
ci / nexus (pull_request) Failing after 1m8s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
1c36805c73
Pass '<workspace-label> · <session-name>' to claude --remote-control so
the claude.ai/code Remote Control session list disambiguates sessions
across multiple workspaces.

The composed identifier is decoupled from the git branch / worktree path
/ tmux window name — those keep using the bare session name (git-ref
safe). Only the Remote Control display label gets the workspace prefix.
checkSessionHealth's /proc grep is updated to match the new identifier.
fix(git-ref): suppress no-control-regex and no-useless-escape on intentional regex
All checks were successful
ci / nexus (pull_request) Successful in 2m7s
ci / images (./nexus, agent-nexus) (pull_request) Successful in 4m1s
ci / images (./worker, nexus-worker) (pull_request) Successful in 2m4s
98e7a786cc
CI ESLint flagged the forbidden-char regex for two intentional patterns:
control chars in the class are exactly what we mean to reject, and the
escaped '[' mirrors the spec literal. Suppress both rules inline with a
short rationale rather than restructuring.
lz merged commit 1af2a38db3 into main 2026-05-15 22:40:27 +02:00
lz deleted branch worktree-issue-10-nexus-worktrees 2026-05-15 22:40:42 +02:00
lz referenced this pull request from a commit 2026-05-16 18:53:47 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!13
No description provided.