Opt-in isolated worktree so parallel implementers don't stomp the shared
tree, hydrated to config parity so a worker differs from the primary only
in LLM provider.
- Worktrees seam (interface) behind SessionManager; GitWorktrees shells git
via ProcessBuilder (non-zero exit -> WorktreeException), FakeWorktrees for
tests. No live git in unit tests.
- acquire() 5-arg overload provisions add -> overlayParity -> spawn(cwd=wt)
-> register, unwinding the worktree on any failure before registration.
4-arg overload and shared-tree behavior unchanged (backward compatible).
- release() removes the checkout but never deletes the branch (it holds the
worker's commits + PR, CB-302).
- overlayParity copies local config (.mcp.json, settings.local.json, .env/
.envrc) into the worktree; tracked ones get --skip-worktree so a worker
can never stage the parity overlay.
- WorkerSession gains nullable worktree/branch; BridgedConfig.Worker gains
parityOverlay (default list) + top-level worktreeRoot.
- bridge_spawn / POST /workers gain an optional worktree(+ticket) arg; the
worker view includes worktree/branch only when non-null.
Verify fixes on the delegated impl: strip trailing dashes in slug()
(^-+|-+$, was ^-+|^-+$); make FakeWorktrees.add a pure fn of the branch
(nonce already unique); MCP worktreeRequest treats blank/"false" string as
no-worktree, matching the REST builder.
162 tests, 0 failures.
A worker now opens the same directory the primary is in, unless told otherwise.
Resolution: explicit spawn cwd → per-profile config cwd → the primary's cwd
(auto-detected from the bridge_spawn caller's PID via lsof -d cwd) → the daemon's
cwd. Never $HOME.
Mechanism (found by live probe, corrects the earlier assumption): an agent.start
pane does NOT inherit its tab's or workspace's cwd — it starts in $HOME. herdr's
agent.start honours an (undocumented) cwd param, so the resolved cwd is threaded
onto agent.start {cwd} (both tab and pane placement), not tab.create.
Surfaces: bridge_spawn {cwd?} + auto-detect via ConnectionIdentity.resolve (peer
PID) + ProcessCwdLookup (lsof); REST POST /workers ?cwd= / body cwd; per-profile
'cwd:' config. Validated live: explicit cwd → worker rooted there; no cwd over
REST → daemon cwd, not $HOME. Also clears the ccs folder-trust prompt when the
project dir is already trusted (see docs/Worker-Startup-and-Trust.md).
The bridge was hard-wired to one worker profile. Config now takes a 'workers'
map keyed by profile name plus 'defaultWorker'; WorkerService holds the map and
gains spawn(profile) (spawn() uses the default). Selection threads through the
surfaces: REST POST /workers ?profile= / {"profile":…} + GET /profiles; MCP
bridge_spawn {profile?} + new bridge_profiles. Each profile's base_url is
guard-checked independently, so gx10 and ollama can run side by side and you
address each worker by its returned sessionId. Backward-compatible: the legacy
singular 'worker:' block still loads as a one-entry profile map.
Spawn a real worker with 'ccs ltms-local' (profile sets CLAUDE_CONFIG_DIR + off-sub base_url; auto mode preconfigured as defaultMode:auto). bridged appends the bridge MCP mount (--mcp-config, inline JSON) and the reply charter (--append-system-prompt) as launch FLAGS — non-invasive, nothing written to the worker's profile (safer than provisioning its config dir, which would clobber it). Identity is connection-based so the mount is shared. config: worker.mcpUrl. Default bind port 8080 -> 8765. The reply charter is guidance; the send timeout catches a non-cooperative worker. 60 tests green, IDE-clean.
Workers now land in their own herdr tab inside a dedicated, shared "worker
space" (workspace.create → tab.create → agent.start{tab_id} → close seed shell
→ rename), instead of splitting the user's currently-focused tab. Placement is
configurable (worker.placement tab|pane, worker.workspace, worker.tabLabel);
a future per-session space is just a distinct label.
New: Workspace/Tab records, WorkspaceControl over workspace.*/tab.*,
AgentControl.start tab_id overload, Agent.tabId.
Lifecycle carefulness:
- ensureWorkspace is a synchronized find-or-create (never double-creates)
- teardown closes the tab ONLY when the worker is its sole pane (never a
shared user tab), resolving the tab via pane.get before closing
- tolerance is precise: only *_not_found is swallowed; real failures surface
- spawn failure closes the orphan tab; post-start cosmetic steps can't orphan
a live worker or fail the spawn
- unique per-worker names (claude-<profile>-<nonce>-<seq>) with retry: herdr
rejects duplicate agent names, and the per-process nonce survives a restart
with lingering workers
herdr facts pinned: agent.start honors tab_id; agent name must be unique;
kind/status are detected from terminal output, not the name.
Reviewed at high effort (multi-agent); all findings addressed. 34 tests green
(29 unit/acceptance + 5 contract vs live herdr 0.7.0). Also bumps wiki.
Spike decided the worker south side in favour of herdr's native agent.*
namespace over pane+send_text. Proven against live herdr 0.7.0:
agent.start takes a first-class env map that reaches the process
environment (ANTHROPIC_BASE_URL confirmed via agent.read), and herdr
tracks each worker's Claude session UUID itself.
- AgentControl: start/send/read/get/status/list + pane.close over agent.*.
- Agent/AgentStatus: projection of herdr agent records (session UUID,
injectable status gate).
- WorkerService: build worker env (base_url/token/model/config dir),
assertWorker BEFORE any herdr call, then agent.start.
- REST: GET /agents (discovery by session UUID), POST /workers (201, or
403 subscription_boundary), DELETE /workers/{paneId}.
- Full stack smoke-tested live: POST->guard->agent.start->new pane,
GET /agents lists it, DELETE closes it.
- Tests: 21 unit/acceptance + 4 contract (incl. a live end-to-end probe
spawn that proves env injection and cleans up its pane).
No agent.stop in herdr (use pane.close); herdr id must be a string;
one request per connection — all pinned by contract tests.
Maven/Java 25 module under bridged/. End-to-end verified against live
herdr 0.7.0 (protocol 14): GET /healthz and GET /sessions serve real
workspace data through the socket client.
- herdr client (CB-101): Unix-socket JSON-RPC via UnixDomainSocketAddress.
Two contract facts pinned by tests against the real daemon:
ids MUST be strings, and herdr is one-shot per connection
(connection-per-call, which also makes the client lock-free).
- subscription guard: worker base_url must be on the off-subscription
allowlist (gx00.gw, ollama.ltms.dev); primary env must carry no base_url.
- config (CB-106): Jackson YAML + Logback; example grounded in ltms-local.
- REST app (CB-104 start): injectable HerdrClient so acceptance tests run
on an ephemeral port with a fake herdr, no daemon/Claude in the loop.
- tests: 17 unit/acceptance (mvn test) + 3 contract (mvn test -Pcontract).