Workdir module — cache clone, worktree lifecycle, per-agent git identity #3

Closed
opened 2026-04-16 22:43:58 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As the runAgent code path, I want a workdir.ts module that owns one cache clone per repo, creates and reuses per-(agent, branch) worktrees, and surfaces git identity + auth helpers, so that we stop re-cloning from Forgejo on every dispatch and worktree state survives between turns.

Acceptance criteria

Module surface (src/workdir.ts)

  • cachePath(repo): string — returns ~/.cache/claude-hooks/repos/<owner>/<name>
  • worktreePath(agent, repo, branch): string — returns ~/.cache/claude-hooks/worktrees/<agent>/<owner>__<name>__<branch> (branch slashes encoded)
  • ensureCacheClone(repo, agent): Promise<void> — clones to cachePath(repo) if missing; idempotent; uses agent's auth via the strategy chosen in the spike issue
  • fetchLatest(repo, agent): Promise<void> — runs git fetch origin inside the cache clone with the agent's auth
  • acquireWorktree(repo, agent, branch): Promise<string> — creates worktree if absent, reuses if present (verify branch matches); returns absolute path
  • releaseWorktree(repo, agent, branch, opts: { keep?: boolean }): Promise<void>keep: true (default) is a no-op; keep: false removes the worktree and prunes
  • gitIdentityEnv(agent): Record<string, string> — returns GIT_AUTHOR_* / GIT_COMMITTER_* env vars
  • gitAuthEnv(agent): Record<string, string> — returns env vars implementing the auth strategy from the spike (likely GIT_ASKPASS + a per-agent script path)

Behaviour

  • All git operations spawn through Bun.spawn; non-zero exits surface as thrown Error with stderr included
  • Cache clone uses --filter=blob:none (or --depth=N) — pick one based on Forgejo support; document the choice
  • Concurrent calls for the same (agent, repo) are safe (worker FIFO already serialises but the module should not assume it)
  • If the cache clone is corrupt (e.g. git status fails), the module nukes it and re-clones — log a warning
  • If a worktree exists at the expected path but is registered for the wrong branch, log + abort (do not silently switch branches)

Tests (src/workdir.test.ts)

  • Unit: acquireWorktree then acquireWorktree again on the same key → second call returns same path, no error
  • Unit: releaseWorktree({ keep: false }) removes the directory and git worktree list no longer shows it
  • Unit: corrupt cache (e.g. delete .git/HEAD) → next ensureCacheClone heals it
  • All tests use a temp directory under tmpdir() and clean up after themselves

Out of scope

  • Wiring the module into runAgent (separate story)
  • Cleanup webhooks (separate story)
  • Sessions or session resumption (separate story)

References

Dependencies

  • Blocked by: #2 (decisions on identity + auth strategy)
  • Blocks: #5, #7
  • Branch off: main (apply spike decisions; spike branch itself is throwaway)
  • Full graph: #10
## User story As the **runAgent code path**, I want a `workdir.ts` module that owns one cache clone per repo, creates and reuses per-(agent, branch) worktrees, and surfaces git identity + auth helpers, so that we stop re-cloning from Forgejo on every dispatch and worktree state survives between turns. ## Acceptance criteria ### Module surface (`src/workdir.ts`) - [ ] `cachePath(repo): string` — returns `~/.cache/claude-hooks/repos/<owner>/<name>` - [ ] `worktreePath(agent, repo, branch): string` — returns `~/.cache/claude-hooks/worktrees/<agent>/<owner>__<name>__<branch>` (branch slashes encoded) - [ ] `ensureCacheClone(repo, agent): Promise<void>` — clones to `cachePath(repo)` if missing; idempotent; uses agent's auth via the strategy chosen in the spike issue - [ ] `fetchLatest(repo, agent): Promise<void>` — runs `git fetch origin` inside the cache clone with the agent's auth - [ ] `acquireWorktree(repo, agent, branch): Promise<string>` — creates worktree if absent, reuses if present (verify branch matches); returns absolute path - [ ] `releaseWorktree(repo, agent, branch, opts: { keep?: boolean }): Promise<void>` — `keep: true` (default) is a no-op; `keep: false` removes the worktree and prunes - [ ] `gitIdentityEnv(agent): Record<string, string>` — returns `GIT_AUTHOR_*` / `GIT_COMMITTER_*` env vars - [ ] `gitAuthEnv(agent): Record<string, string>` — returns env vars implementing the auth strategy from the spike (likely `GIT_ASKPASS` + a per-agent script path) ### Behaviour - [ ] All git operations spawn through `Bun.spawn`; non-zero exits surface as thrown `Error` with stderr included - [ ] Cache clone uses `--filter=blob:none` (or `--depth=N`) — pick one based on Forgejo support; document the choice - [ ] Concurrent calls for the same `(agent, repo)` are safe (worker FIFO already serialises but the module should not assume it) - [ ] If the cache clone is corrupt (e.g. `git status` fails), the module nukes it and re-clones — log a warning - [ ] If a worktree exists at the expected path but is registered for the wrong branch, log + abort (do not silently switch branches) ### Tests (`src/workdir.test.ts`) - [ ] Unit: `acquireWorktree` then `acquireWorktree` again on the same key → second call returns same path, no error - [ ] Unit: `releaseWorktree({ keep: false })` removes the directory and `git worktree list` no longer shows it - [ ] Unit: corrupt cache (e.g. delete `.git/HEAD`) → next `ensureCacheClone` heals it - [ ] All tests use a temp directory under `tmpdir()` and clean up after themselves ## Out of scope - Wiring the module into `runAgent` (separate story) - Cleanup webhooks (separate story) - Sessions or session resumption (separate story) ## References - Discussion: chat history, "Phase 1 — worktree cache" plan - `git worktree` docs: <https://git-scm.com/docs/git-worktree> ## Dependencies - **Blocked by:** #2 (decisions on identity + auth strategy) - **Blocks:** #5, #7 - **Branch off:** `main` (apply spike decisions; spike branch itself is throwaway) - **Full graph:** #10
Sign in to join this conversation.
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
charles/claude-hooks#3
No description provided.