Files
fleetd/docs/Worker-Git-Workflow.md
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

183 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Worker Git Workflow — worktree · branch · PR
**Status:** design (defining the fleet's working model). Builds on the
[CB-301 session manager](CB-301-Session-Manager.md) 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:
```mermaid
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
```mermaid
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 `curl`s `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.