Files
fleetd/docs/Worker-Git-Workflow.md
T
Dai Ha ecc590f344 CB-632 unit 5: rename the daemon and classes in the docs prose
Part of #145 (CB-632). Documentation only, plus one internal literal.

Unit 1 renamed the package and classes, which left every doc describing
classes that no longer exist. This fixes the prose across README.md,
docs/ and bridged/docs/ -- 18 files.

Renamed: dev.ltms.bridged -> dev.ltms.fleet, the five class names, and
"bridged" where it names the daemon as a product rather than a path.

Also renamed two literals, because a doc that disagrees with the code is
worse than one that is out of date:

  - bridged-local-noauth -> fleetd-local-noauth. A placeholder apiKey
    OpenCodeLauncher sends when a profile resolves no token, to a local
    endpoint that does not check it. No test asserts the old string.
  - the vnd.ltms.bridged.* media type in the M4 design doc. It appears
    in no Java file, so nothing implements it yet.

Deliberately NOT renamed, because each is still literally true today and
changes only at the cutover:

  - paths: bridged/, bridged.yaml, bridged.example.yaml, bridged.jar,
    .bridged-worktrees, deploy/dev.ltms.bridged.plist,
    scripts/redeploy-bridged.sh, bridged-launchd-wrapper.sh
  - bridged_* metric names -- renaming these after the monitoring is
    wired would break dashboard continuity, so they move before it is
  - bridge_* MCP tool names, which answer alongside fleet_* on purpose
  - BRIDGED_* environment variables, read by a file outside this repo

Method note: perl, not sed. BSD sed has no \b and no lookaround, and a
word-boundary expression there fails silently. The prose replace uses
(?<![\w./-])bridged(?![\w./-]) so it cannot touch a path or an
identifier, then every remaining hit was read by hand.

Verified: mvn clean install green, 51 classes, 878 tests, 0 failures.
2026-08-23 06:46:34 +02:00

11 KiB
Raw Blame History

Worker Git Workflow — worktree · branch · PR

Status: design (defining the fleet's working model). Builds on the CB-301 session manager and the one-shot/no-reuse decision.

Guiding principle — a worker is a full peer of the primary

The whole point of the bridge is the same Claude Code agent running against a different LLM provider. A worker must be functionally identical to the primary in context and knowledge — same project + user CLAUDE.md, same skills, same memory, same MCP tools, same local/private settings — and differ only in ANTHROPIC_BASE_URL/ANTHROPIC_MODEL. The worktree exists solely for git isolation (a branchable checkout for commits + code reference). It must never strip the worker of the configuration a main-tree session has. Config parity is a hard requirement, not a nice-to-have.

Decisions

  • One-shot, no reuse (CB-301) — each task gets a fresh worker, torn down after. The PR is the durable artifact; no context is carried across workers.
  • Worktree provisioned by the daemon — SessionManager creates a dedicated git worktree + branch per session, hydrates it to full config parity (below), and tears it down on release.
  • Worker opens its own PR — the worker commits, pushes its branch, and opens the PR/MR itself, returning the PR URL in its fleet_reply.

Why worktrees (the hazard being fixed)

Today every bridge worker inherits the primary's own working tree as its cwd (WorkerService cwd resolution → caller cwd). A single worker editing at a time is safe, but two parallel implementers would stomp each other's files. A worktree per session gives each worker an isolated checkout on its own branch — the precondition for fanning out implementation work.

Config parity — the worktree is code-only; it must NOT strip the worker

The trap: git worktree add creates a checkout of tracked files only. Untracked / gitignored files do not come along. So a worker moved from the primary's main tree into a bare worktree silently loses exactly the local/private configuration that makes it a full peer — while the shared-tree it runs in today gives it all of this for free. Moving to worktrees must preserve that, not regress it.

Classify every source of "what a main session knows", by whether a worktree keeps it:

flowchart TD
    subgraph keeps["Inherited automatically — no action"]
        A["User-global config<br/>~/.claude/CLAUDE.md, ~/.ccs memory"]:::ok
        B["Tracked project config<br/>CLAUDE.md, committed .claude/skills, committed .mcp.json"]:::ok
        C["CLAUDE_CONFIG_DIR<br/>(daemon already injects per profile)"]:::ok
        D["Bridge MCP<br/>(injected via --mcp-config launch flag)"]:::ok
    end
    subgraph gap["LOST by a bare worktree — must be hydrated"]
        E["settings.local.json<br/>local .claude/* overrides"]:::warn
        F["local .mcp.json mods<br/>(the M .mcp.json in git status)"]:::warn
        G[".env / .envrc / direnv<br/>local secrets + tokens"]:::warn
        H["any other gitignored local config"]:::warn
    end
    keeps --> R["Worker = full peer of primary"]:::goal
    gap -->|"overlay step at provision"| R
    classDef ok fill:#2f855a,stroke:#22543d,color:#ffffff;
    classDef warn fill:#b7791f,stroke:#7b341e,color:#ffffff;
    classDef goal fill:#2b6cb0,stroke:#2a4365,color:#ffffff;

Green is inherited by path (home dir / CLAUDE_CONFIG_DIR) or lives in tracked files that the worktree checks out anyway. Amber is the real gap — untracked local config the worktree drops.

Mechanism — worktree hydration (part of SessionManager.acquire, after git worktree add):

  1. Inherit by path, don't copy — keep the worker's $HOME, CLAUDE_CONFIG_DIR, and memory dir identical to the primary's. Everything home-scoped (user CLAUDE.md, memory, auth, global skills) is already parity for free; only cwd-relative project-local files are the gap.
  2. Overlay the untracked project-local set from the primary tree into the worktree — a defined, configurable list: settings.local.json (+ any local .claude/*), the locally-modified .mcp.json, .env/.envrc, and any other gitignored config the primary depends on. Symlink (read-only parity, stays live, nothing to go stale) rather than copy where possible; copy only what a worker may write.
  3. Never overlay the git plumbing — the worktree's own .git file/branch is what gives isolation; that's the one thing that must differ from the main tree.

The overlay set lives in config (FleetConfig.Worker.parityOverlay — a list of repo-relative paths, with sane defaults) so it's auditable and per-repo tunable.

Trust note (deliberate). Hydrating local config means the primary's local secrets/tokens (.env, .mcp.json auth, gitea token) become visible to an off-subscription worker running against a third-party model endpoint. That is the accepted consequence of "workers must be full peers" — but it is a real trust expansion over a worker that only sees tracked code. Keep the overlay list explicit and minimal; don't blanket-symlink the whole tree. .mcp.json and wiki/ remain excluded from all worker commits regardless of being present for reference.

Lifecycle

sequenceDiagram
    autonumber
    participant P as Primary
    participant SM as SessionManager (daemon)
    participant G as git / gitea
    participant W as Worker

    P->>SM: acquire(ticket, profile)
    SM->>G: git worktree add wt -b worker/ticket-nonce main
    SM->>SM: overlay parity config into wt
    SM->>W: spawn (cwd = wt, on its branch)
    Note over W: implement in the isolated worktree
    W->>G: git commit + git push (SSH, same user)
    W->>G: open PR (branch to main)
    W-->>P: fleet_reply (prUrl, branch, summary, tests)
    P->>SM: release(paneId)
    SM->>G: git worktree remove wt
    Note over G: branch + PR persist for review/merge
    P->>G: review PR, merge on green

The worker's "checkpoint" (CB-302) is exactly steps 6–8: commit → push → open PR. This replaces the earlier STATE.md idea — a PR is reviewable, mergeable, and self-describing.

Infra facts (verified this session)

  • Remote: ssh://git@git.ltms.dev:2224/fleet/fleetd.git (gitea). Push is over SSH — a worker running as the same user with the same keys can git push with no extra credential.
  • gitea is NOT in the project .mcp.json (only jetbrains, intellij-index, fleetd). The primary's gitea MCP comes from a global/user config, so workers do not inherit it. A worker gets only the bridge MCP mounted (via --mcp-config launch flag).
  • No gitea CLI (tea) installed; glab is present but is the GitLab CLI (wrong backend).

Implication: git push is free for workers; only PR creation needs a new mechanism.

Open decision — how the worker creates the PR

Option Mechanism Trade-off
A. gitea REST + token Worker curls POST /api/v1/repos/fleet/fleetd/pulls with a scoped token injected by the daemon into the worker env Minimal, no new server; token lives in the off-subscription worker's env (scope it tightly)
B. mount gitea MCP into workers Add the gitea MCP to the worker's --mcp-config alongside bridge Clean tool call, but the gitea MCP's own auth/token must be provisioned per worker; more moving parts
C. install tea CLI Worker runs tea pr create with a token Another dependency to install + configure; same token question as A

Recommendation: A (gitea REST + a repo-scoped token). Smallest surface, reuses SSH for push, and the token is a single scoped secret the daemon injects like it already injects ANTHROPIC_AUTH_TOKEN. The implementer skill wraps the curl in one documented step.

Trust / token scope (the real cost of "worker opens its own PR")

  • Off-subscription workers already could push (SSH, same user). The incremental grant is PR-create, i.e. a gitea API token.
  • Scope the token minimally: the fleet/fleetd repo, write:repository (create branch + PR), not merge/admin/org. A leaked token can open PRs, not merge them — the primary/human is still the merge gate.
  • Inject via the daemon (env var, e.g. GITEA_TOKEN), never written to the worker's config dir — same non-invasive pattern as the ANTHROPIC token. Guard is unaffected (it concerns ANTHROPIC_BASE_URL, not git).

Implementation plan

Piece Where Notes
Worktree provision/teardown CB-301 ext — SessionManager.acquire/release; WorkerSession gains worktree, branch daemon shells out to git worktree add/remove
Config-parity overlay CB-301 ext — SessionManager.acquire, after git worktree add symlink/copy the parityOverlay set into the worktree so the worker is a full peer; this is what makes worktrees viable, not a dead-end
Overlay config FleetConfig.Worker.parityOverlay — repo-relative paths, sane defaults auditable, per-repo tunable; keep explicit + minimal (trust)
Branch naming worker/<ticket-slug>-<nonce> off main (or a configured base) one branch per session
Commit + push + PR handoff CB-302 — worker-driven, guided by the skill push = SSH; PR = option A
Implementer skill .claude/skills/implementer/SKILL.md worktree-aware playbook (see below); mounts automatically since workers inherit repo cwd
gitea token injection WorkerService env + FleetConfig repo-scoped, minimal perms
PR review + merge Primary (has gitea MCP + judgment) merge on green; the human/primary gate stays

Implementer skill (outline)

A worker-facing playbook (sibling to the existing reviewer skill):

  1. You are in a git worktree on a dedicated branch — check git status/git branch; do all work here, never on main.
  2. Implement the task; keep commits focused and message them clearly.
  3. Push your branch (git push -u origin HEAD).
  4. Open a PR to main (option A curl, or the decided mechanism) with a title/body describing the change and referencing the ticket.
  5. Reply via fleet_reply with the PR URL, branch name, files changed, and test names — that reply is the whole handoff.
  6. Do not merge; do not touch .mcp.json or wiki/.

Sequencing

  1. Finish + verify CB-301 core (in flight) — registry/FSM.
  2. Extend CB-301 with worktree provisioning (this doc) once the PR mechanism is chosen.
  3. Add the implementer skill + token injection.
  4. CB-302 = wire the worker checkpoint (commit/push/PR) as the release-time handoff.