76e4b577a0
Rename the eleven MCP tool names (bridge_ack/ask/list/poll/profiles/reply/ send/spawn/status/stop/whoami) to their fleet_* names across the Markdown documentation. fleet_* is written as the normal name; one deprecation note in README.md says bridge_* still works for one release. docs/MCP-Contract.md is renamed only inside section 6 (lines 210-293): sections 1-5 and 7-11 are stale pre-build design text (CB-609) and are deliberately left with old names so dead text does not look maintained. Also renames e2e/bridge_ask_transcript.md to e2e/fleet_ask_transcript.md to match its content. CLAUDE.md, wiki/, plugin/skills/setup/SKILL.md and .claude/skills/port-to-opencode/SKILL.md are owned by other units and are untouched.
183 lines
11 KiB
Markdown
183 lines
11 KiB
Markdown
# 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 (`BridgedConfig.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/lms/claude-bridge.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`, `bridged`). 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/lms/claude-bridge/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 `lms/claude-bridge` 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 | `BridgedConfig.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 + `BridgedConfig` | 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.
|