feat(workdir): cache clone + per-agent worktree lifecycle #13
Closed
claude-desktop
wants to merge 0 commits from
boss/3 into main
pull from: boss/3
merge into: charles:main
charles:main
charles:chore/sync-pre-push-from-forge-base
charles:fix/flows-yaml-dispatch-identity
charles:feat/board-tap-to-assign
charles:dev/1107
charles:code-lead/1106
charles:code-lead/1108
charles:dev/1104
charles:code-lead/1103
charles:code-lead/1080
charles:dev/1087
charles:feat/flows-yaml-ci-events
charles:chore/board-drop-stalled-and-density-controls
charles:fix/flows-yaml-routes-always-register
charles:flows-yaml/api-defaults
charles:dev/1023
charles:fix/event-log-history-bleed
charles:fix/janitor-fix-ci-logs-and-cap
charles:dev/1022
charles:fix/board-card-provider
charles:code-lead/1036
charles:dev/1025
charles:code-lead/1020
charles:dev/1017
charles:code-lead/1026
charles:feat/web-shortcut-registry-1018
charles:dev/1015
charles:code-lead/1009
charles:code-lead/1008
charles:dev/975
charles:dev/969
charles:dev/973
charles:dev/967
charles:code-lead/968
charles:code-lead/953
charles:dev/970
charles:dev/976
charles:code-lead/966
charles:code-lead/956
charles:code-lead/951
charles:dev/962
charles:dev/963
charles:dev/977
charles:dev/955
charles:dev/983
charles:dev/961
charles:dev/974
charles:code-lead/950
charles:code-lead/939
charles:dev/941
charles:dev/940
charles:dev/937
charles:dev/938
charles:dev/936
charles:dev/935
charles:feat/web-i18n-fr-locale
charles:feat/spec-editor-ui-polish
charles:chore/drop-legacy-compat
charles:fix/skills-drop-preview-pane
charles:fix/882-skills-safety-rail
charles:dev/911
charles:dev/909
charles:dev/923
charles:dev/917
charles:dev/915
charles:feat/879-sr11-m2-drop-legacy-skill
charles:code-lead/873
charles:dev/881
charles:code-lead/869
charles:dev/867
charles:code-lead/845
charles:code-lead/843
charles:code-lead/844
charles:dev/837
charles:dev/861
charles:dev/849
charles:code-lead/837
charles:code-lead/842
charles:fix/dedup-rebase-inflight
charles:dev/838
charles:code-lead/847
charles:dev/833
charles:code-lead/848
charles:pr/838
charles:code-lead/841
charles:feat/settings-save-bar/836
charles:code-lead/840
charles:dev/846
charles:code-lead/839
charles:dev/832
charles:fix/board-sse-stale-cache
charles:dev/834
charles:dev/835
charles:feat/settings-breadcrumbs
charles:feat/forge-oauth-credentials
charles:refactor/service-config-consolidation
charles:feat/agent-tokens-to-secrets
charles:feat/gitlab-oauth-to-db
charles:feat/authelia-rip-and-voice-fixes
charles:fix/rebase-storm-and-dead-letter
charles:code-lead/797
charles:code-lead/796
charles:dev/811
charles:code-lead/798
charles:dev/810
charles:code-lead/795
charles:dev/808
charles:code-lead/794
charles:dev/805
charles:dev/802
charles:dev/803
charles:feat/avatar-menu-settings-entry
charles:feat/per-agent-token-tracking
charles:dev/793
charles:dev/747
charles:dev/752
charles:code-lead/790
charles:code-lead/759
charles:dev/756
charles:dev/760
charles:dev/741
charles:dev/767
charles:dev/740
charles:dev/709
charles:dev/644
charles:dev/637
charles:boss/614
charles:dev/600
charles:dev/611
charles:dev/585
charles:fix/login-bonus-fixes
charles:boss/544
charles:dev/542
charles:refactor/api-prefix-and-session-gate
charles:dev/489
charles:boss/531
charles:boss/518
charles:dev/499
charles:boss/516
charles:dev/530
charles:dev/517
charles:dev/519
charles:dev/515
charles:dev/522
charles:dev/503
charles:dev/471
charles:boss/329
charles:dev/417
charles:dev/418
charles:dev/402
charles:boss/327
charles:dev/334
charles:dev/332
charles:boss/326
charles:boss/325
charles:dev/331
charles:boss/324
charles:boss/323
charles:boss/322
charles:dev/294
charles:test/s11-task-analytics
charles:dev/262
charles:boss/270
charles:dev/268
charles:foreman/ui-consolidation-spec
charles:dev/234
charles:boss/196
charles:boss/176
charles:boss/164
charles:fix/124-session-persist-bind
charles:boss/52
charles:dev/87
charles:boss/73
charles:dev/77
charles:dev/81
charles:dev/82
charles:boss/79
charles:dev/42
charles:dev/35
charles:boss/7
No reviewers
Labels
Clear labels
area:agents
Agent types, pool scheduling, per-instance config
area:dashboard
Dashboard UI and observability surfaces
area:database
DB layer — schema, migrations, ORM, raw SQL
area:design
UI/UX mockup work — routes to designer agent
area:design-review
Design review dispatch — routes to design-reviewer agent
area:flows
Flow runner — YAML loader, executor, op registry, expression eval
area:infra
Deployment, isolation, containers, systemd units
area:meta
Tracking, scaffolding, project setup
area:security
Security — routes to reviewer-security (opus)
area:sessions
Session-id store, Claude SDK resume logic
area:webhook
Forgejo webhook routing and handlers
area:workdir
Clone cache, worktrees, git identity
security
Security-sensitive issue
type:bug
Bug
type:chore
Chore
type:meta
Tracking or decisions, not implementation work
type:user-story
User story
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
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!13
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds
src/workdir.ts— the module that owns one partial cache clone per repo and per-(agent, branch) worktrees under~/.cache/claude-hooks/..., plus env-var helpers for git identity and auth. This is the on-disk foundation for swapping the per-taskmkdtempclone inrunAgentwith reusable worktrees (tracked separately by #5).Surface
cachePath(repo)/worktreePath(agent, repo, branch)— path helpers; branch slashes percent-encoded.ensureCacheClone(repo, agent)— idempotent clone; self-heals a corrupt cache by nuking + re-cloning (with a warning).fetchLatest(repo, agent)—git fetch origin --prunewith agent auth.acquireWorktree(repo, agent, branch)— creates on first call, reuses on subsequent calls; aborts if an existing worktree is on the wrong branch.releaseWorktree(repo, agent, branch, { keep })— defaultkeep: trueis a no-op;keep: falseremoves + prunes.gitIdentityEnv(agent)/gitAuthEnv(agent)— env vars for the Claude Agent SDKquery({ env })and forBun.spawngit calls.Applied spike (#2) decisions
--filter=blob:none(partial clone; Forgejo ≥ 1.19 supports it).GIT_AUTHOR_*/GIT_COMMITTER_*) rather thanextensions.worktreeConfig— simpler, works cleanly withquery({ env }).GIT_ASKPASSscript that readsCLAUDE_HOOKS_GIT_TOKENfrom the environment. Nothing lands in.git/config.GIT_TERMINAL_PROMPT=0prevents an interactive fallback.Tests
src/workdir.test.tsexercises the module against a local bare remote:acquireWorktreetwice on the same key returns the same path with no error.releaseWorktree({ keep: false })removes the dir and drops it fromgit worktree list; defaultkeepis a no-op..git/HEAD) is detected on the nextensureCacheCloneand healed.Cache root is overridable via
CLAUDE_HOOKS_CACHE_DIR, remote base viaCLAUDE_HOOKS_FORGEJO_URL, so tests scope everything to atmpdir()sandbox and clean up inafterEach.Test plan
bunx tsc --noEmitcleanbunx biome check src/cleanbun test— 27 pass, 0 failCloses #3
Code Review — Changes Required
All required module surface functions are present and correctly named. Tests cover the three explicitly required scenarios. Four issues need to be addressed before merging.
Test utility bug:
restoreEnvsets""instead of deleting the keyWhen a key was not present before the test, this leaves it as an empty string rather than removing it. After the first test suite run,
CLAUDE_HOOKS_FORGEJO_URL(and any other non-defaulted env var) will be""instead ofundefinedfor subsequent tests.cacheRoot()happens to survive this because"" || fallbackworks, but any future env var without a falsy-safe default would see""as a set value.Fix:
Bug: concurrent
ensureCacheClonecalls are not safe — AC explicitly requires thisAC: "Concurrent calls for the same
(agent, repo)are safe (worker FIFO already serialises but the module should not assume it)."Two concurrent calls both reach
await exists(path)→false, then both attemptgit clone. The second clone will fail withdestination path '...' already existsand throw — crashing the second dispatch.The same race exists in
acquireWorktreeat line 249: concurrent calls both missexists(path), then the secondgit worktree addfails because the directory appeared mid-flight.Fix — add a per-repo async mutex using a
Map<string, Promise<void>>:Apply the same pattern in
acquireWorktree.Bug:
askpassEnsuredmodule-level flag is never reset — write silently skipped after cache-root changeaskpassEnsuredis set totrueon the first successful write and never cleared. WhenCLAUDE_HOOKS_CACHE_DIRchanges between calls (which happens on every test invocation since eachbeforeEachsets a fresh temp path),ensureAskpassScript()returns immediately without writing the script to the new location. Anygit clone/git fetchthat runs next getsGIT_ASKPASSpointing at a non-existent file — silent credential failure on HTTPS remotes.Tests pass today only because they use
file://remote URLs, which never invokeGIT_ASKPASS.Fix — drop the boolean and check the file on disk instead:
This is idempotent (same content, same mode), so the extra
staton the fast path costs almost nothing.Minor: wrong-branch abort missing
console.warn— AC says "log + abort"The behaviour AC states: "If a worktree exists at the expected path but is registered for the wrong branch, log + abort (do not silently switch branches)."
The code throws but does not log. Callers may propagate the error without adding context, leaving nothing in the service log at the point of detection.
Fix:
7efb52b1c3f22a488642Pull request closed