Skip to content

feat(worktrees): task-pinned worktrees under worktreeNaming "task-id" - #2233

Merged
gsxdsm merged 3 commits into
mainfrom
feature/task-pinned-worktrees
Jul 16, 2026
Merged

feat(worktrees): task-pinned worktrees under worktreeNaming "task-id"#2233
gsxdsm merged 3 commits into
mainfrom
feature/task-pinned-worktrees

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds task-pinned worktrees for worktreeNaming: "task-id". Under task-id naming, a task is pinned to exactly one derivable directory <worktreesDir>/<lowercased-task-id> (e.g. .worktrees/fn-7996) for its entire lifecycle — removing the ambiguity that let stale/foreign task.worktree pointers strand a task (the FN-7996 shape).

recycleWorktrees stays fully functional and is mutually exclusive with task-id pinning: the two can't be enabled together.

Behavior

  • Pinned acquisition (worktreeNaming: "task-id", recycling off): acquireTaskWorktree runs derive → validate → reuse-or-recreate at the derived path — warm-reuse when the dir is a registered, usable worktree on the task's own branch; otherwise reclaim-in-place (removeWorktree + recreate at the SAME path, never a sibling name). A disagreeing task.worktree cache self-corrects and emits a new worktree:pin-rederived audit event, without consuming worktree-session retries. The recycle pool is never consulted in pinned mode.
  • Mutual exclusivity: enabling both recycleWorktrees and worktreeNaming: "task-id" is rejected at the settings-write boundary — HTTP 400 at PUT /settings, and an Error backstop in store.updateSettings covering the CLI and every other writer (assertWorktreeNamingRecycleExclusive). The runtime also gates pinned mode on !recycleWorktrees, so a legacy on-disk config carrying both degrades safely to recycling.
  • Settings UI: the Settings → Worktrees panel enforces the exclusivity bidirectionally — the Recycle worktrees toggle is disabled while naming is Task ID, and the naming select is disabled while recycling is on — so the conflicting state is unreachable, with help text explaining why.
  • Byte-inert for the rest: random/task-title naming and the recycle pool (incl. merger.ts release) are unchanged; worktrunk-managed layouts bypass pinning.

Acceptance criteria (from the plan)

  1. ✅ Pinned task dispatched N times only ever touches <worktreesDir>/<task-id> on its own branch
  2. ✅ No code path can hand task A's dir to task B (pool bypassed; path derived from task id)
  3. ✅ FN-7996 stale/foreign task.worktree self-corrects at next dispatch (worktree:pin-rederived) without consuming session retries
  4. ✅ Non-pinned modes with recycleWorktrees: true|false are byte-identical (existing pool tests pass unchanged)
  5. ✅ Stale same-name dir (crash leftover / archive→restore) reclaimed in place, never suffixed
  6. ✅ Docs updated (settings-reference, architecture, worktreeNaming type doc); changeset (minor, feature); FNXC comments encode the invariant

Files

  • packages/engine/src/worktree-pinning.ts — new pure helpers (isTaskPinnedWorktreeNaming, pinnedWorktreePathForTask)
  • packages/engine/src/worktree-acquisition.ts — pinned branch + branch-match reclaim-in-place
  • packages/engine/src/run-audit.tsworktree:pin-rederived audit type
  • packages/core/src/settings-validation.ts (+ index.ts, task-store/settings-ops.ts) — mutual-exclusion validator + wiring
  • packages/dashboard/src/routes/register-settings-memory-routes.ts — 400 on conflict
  • packages/dashboard/app/components/settings/sections/WorktreesSection.tsx (+ packages/i18n/locales/en/app.json) — bidirectional UI exclusivity
  • packages/core/src/types.ts, docs/*, .changeset/*

Verification

  • New tests: engine worktree-pinning (5) + worktree-acquisition-pinned (7); core worktree-naming-recycle-exclusive (2); dashboard settings-route 400 (3) + WorktreesSection UI exclusivity (3)
  • Regression sweep green: 194 engine worktree/acquisition/pool/executor/merger-release tests, core settings tests, dashboard i18n/settings-section tests
  • tsc --noEmit clean for @fusion/core and @fusion/engine; changed source files clean; eslint clean
  • pnpm verify:fast PASS (build + scoped typecheck + boot smoke)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Task ID worktree naming, providing each task with a stable, deterministic worktree directory.
    • Automatically reuses valid pinned worktrees and recreates stale or conflicting ones at the same path.
    • Added clear settings controls and validation for incompatible Task ID naming and worktree recycling options.
  • Documentation

    • Updated worktree architecture, settings reference, and in-app guidance to explain pinned worktrees and configuration constraints.
  • Bug Fixes

    • Improved recovery from stale or incorrect worktree assignments without consuming session retries.

When worktreeNaming is "task-id", each task is pinned to one derivable
directory <worktreesDir>/<lowercased-task-id> for its whole lifecycle.
Acquisition runs derive → validate → reuse-or-recreate at that same path
(never suffixed); a disagreeing task.worktree cache self-corrects and emits
worktree:pin-rederived. The recycle pool is untouched for random/task-title.

Task pinning and recycleWorktrees are mutually exclusive: enabling both is
rejected at the settings-write boundary (assertWorktreeNamingRecycleExclusive,
enforced in store.updateSettings + dashboard PUT /settings), and the runtime
gates pinned mode on !recycleWorktrees so a legacy config carrying both
degrades safely to recycling. Worktrunk-managed layouts bypass pinning.

- new packages/engine/src/worktree-pinning.ts pure helpers
- worktree-acquisition.ts pinned branch + branch-match reclaim-in-place
- run-audit.ts worktree:pin-rederived type
- core settings-validation mutual-exclusion validator + wiring
- docs (settings-reference, architecture), types doc, changeset
- unit + acquisition + settings-validation + route 400 tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@gsxdsm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 391d457d-f366-450c-b74e-d7e83030b1a3

📥 Commits

Reviewing files that changed from the base of the PR and between 597b761 and c474973.

📒 Files selected for processing (4)
  • packages/dashboard/src/routes/__tests__/register-settings-memory-worktrunk.test.ts
  • packages/dashboard/src/routes/register-settings-memory-routes.ts
  • packages/engine/src/__tests__/worktree-acquisition-pinned.test.ts
  • packages/engine/src/worktree-acquisition.ts
📝 Walkthrough

Walkthrough

Changes

Task-pinned worktree behavior

Layer / File(s) Summary
Settings exclusivity contract and controls
packages/core/src/settings-validation.ts, packages/core/src/task-store/..., packages/dashboard/..., packages/i18n/...
Adds shared conflict validation for recycleWorktrees with worktreeNaming: "task-id", applies it to settings writes and routes, and updates the UI, tests, and localized guidance.
Pinned path derivation and acquisition
packages/engine/src/worktree-pinning.ts, packages/engine/src/worktree-acquisition.ts, packages/engine/src/run-audit.ts, packages/engine/src/__tests__/*
Derives stable task paths, validates branches, corrects stale metadata, reuses valid directories, and reclaims or recreates invalid pinned worktrees with audit events.
Documentation and release wording
.changeset/task-pinned-worktrees.md, docs/*.md, packages/i18n/locales/en/app.json
Documents deterministic task paths, recycling incompatibility, worktrunk bypass behavior, and settings semantics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Task
  participant acquireTaskWorktree
  participant WorktreePool
  participant TaskStore
  Task->>acquireTaskWorktree: request worktree
  acquireTaskWorktree->>WorktreePool: derive and inspect task-pinned path
  WorktreePool-->>acquireTaskWorktree: valid or stale directory
  acquireTaskWorktree->>TaskStore: correct task.worktree metadata
  acquireTaskWorktree->>WorktreePool: reuse or recreate same pinned path
Loading

Possibly related PRs

  • Runfusion/Fusion#1710: Extends the workspace acquisition path that calls acquireTaskWorktree, coupling it to the new pinned-acquisition behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: task-pinned worktrees for worktreeNaming "task-id".
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/task-pinned-worktrees

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds task-pinned worktrees for Task ID naming. The main changes are:

  • Derives one stable worktree path per task.
  • Reuses valid pinned worktrees and reclaims stale paths in place.
  • Prevents Task ID naming from being combined with worktree recycling.
  • Adds API, store, and Settings UI validation.
  • Adds audit events, tests, documentation, and a release changeset.

Confidence Score: 5/5

This looks safe to merge.

  • Worktree removal failures stop acquisition before resume metadata is cleared.
  • The legacy settings conflict can be repaired by disabling recycling.
  • No blocking issues were found in the updated code.

Important Files Changed

Filename Overview
packages/engine/src/worktree-acquisition.ts Adds pinned acquisition, branch validation, warm reuse, and safe reclaim-in-place behavior.
packages/engine/src/worktree-pinning.ts Adds deterministic helpers for deriving task-pinned worktree paths.
packages/core/src/settings-validation.ts Adds shared validation for incompatible recycling and Task ID naming settings.
packages/dashboard/src/routes/register-settings-memory-routes.ts Rejects conflicting settings updates with a client error.
packages/dashboard/app/components/settings/sections/WorktreesSection.tsx Prevents new settings conflicts while allowing legacy conflicts to be repaired.

Reviews (3): Last reviewed commit: "Address PR review feedback (#2233)" | Re-trigger Greptile

Comment thread packages/engine/src/worktree-acquisition.ts
Comment thread packages/dashboard/app/components/settings/sections/WorktreesSection.tsx Outdated
- worktree-acquisition.ts (P1): pinned reclaim-in-place now fails loud when
  removeWorktree fails (e.g. active-session removal) instead of clearing
  sessionFile and recreating over the still-occupied path — preserves resume
  metadata and lets executor retry/self-healing recover.
- WorktreesSection.tsx (P2): legacy-conflict escape hatch — when a stored config
  already has recycleWorktrees + worktreeNaming:"task-id", keep the recycle
  toggle enabled+checked (mirroring the runtime "recycling wins" backstop) so the
  operator can turn it off and unlock the naming select; never lock both.
- settings-sections test: cover the legacy-conflict escape hatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/dashboard/src/routes/register-settings-memory-routes.ts (1)

764-772: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Map the backstop conflict error to a 400 status.

The explicit pre-check added above (lines 707-721) can be bypassed if a user sends recycleWorktrees: null to clear the project setting. In that scenario, isRecycleWorktreeNamingConflict evaluates null === true as false, allowing the patch to proceed.

When it proceeds, the updateSettingsImpl backstop correctly evaluates the null deletion against the global fallback and safely throws an error if the fallback combination is invalid. However, because this catch block does not recognize the "mutually exclusive" error string, it will return a 500 Internal Server Error instead of a clean 400 Bad Request.

Include the conflict message in this mapping to ensure a 400 status is reliably returned for all conflict edge cases.

♻️ Proposed fix to map the error and reduce duplication
     } catch (err: unknown) {
       if (err instanceof ApiError) {
         throw err;
       }
-      const status = typeof (err instanceof Error ? err.message : String(err)) === "string" && (
-        (err instanceof Error ? err.message : String(err)).includes("modelPresets") || (err instanceof Error ? err.message : String(err)).includes("must include both provider and modelId")
-      ) ? 400 : 500;
-      throw new ApiError(status, err instanceof Error ? err.message : String(err));
+      const errorMessage = err instanceof Error ? err.message : String(err);
+      const status = typeof errorMessage === "string" && (
+        errorMessage.includes("modelPresets") ||
+        errorMessage.includes("must include both provider and modelId") ||
+        errorMessage.includes("mutually exclusive")
+      ) ? 400 : 500;
+      throw new ApiError(status, errorMessage);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/src/routes/register-settings-memory-routes.ts` around
lines 764 - 772, Update the catch block’s status mapping around the
register-settings route to classify the backstop “mutually exclusive” conflict
error as 400, alongside the existing modelPresets and provider/modelId checks.
Reuse a single normalized error-message value for the string conversion and
conflict checks, while preserving ApiError passthrough and 500 status for
unrelated errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/engine/src/__tests__/worktree-acquisition-pinned.test.ts`:
- Around line 5-7: Add a dated `FNXC:<Area-of-product>` heading in the comment
above the pinned-mode test seam explanation, using the required
`yyyy-MM-dd-hh:mm` format. Keep the existing rationale intact and make the
heading concisely identify the recorded testing decision.

In `@packages/engine/src/worktree-acquisition.ts`:
- Around line 174-188: Update pinnedWorktreeBranchMatches so failures from
canonicalizePath or getRegisteredWorktreeBranches propagate to the caller
instead of being caught and converted to false. Return false only when branch
enumeration succeeds and the matching worktree is absent or has a different
branch.
- Around line 542-548: Update the existing pinned-worktree reuse path in the
acquisition flow around classification.ok and branchMatches so that, when
adopting an orphaned worktree, it persists the task’s worktree path and resumed
branch metadata before returning reuseWarmWorktree. Preserve the current reuse
behavior for already-assigned tasks and ensure both task.worktree and
task.branch are restored.

---

Outside diff comments:
In `@packages/dashboard/src/routes/register-settings-memory-routes.ts`:
- Around line 764-772: Update the catch block’s status mapping around the
register-settings route to classify the backstop “mutually exclusive” conflict
error as 400, alongside the existing modelPresets and provider/modelId checks.
Reuse a single normalized error-message value for the string conversion and
conflict checks, while preserving ApiError passthrough and 500 status for
unrelated errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 95cfe234-1270-4d10-9f44-fd2be32ff9f6

📥 Commits

Reviewing files that changed from the base of the PR and between b891e3a and 597b761.

📒 Files selected for processing (18)
  • .changeset/task-pinned-worktrees.md
  • docs/architecture.md
  • docs/settings-reference.md
  • packages/core/src/__tests__/worktree-naming-recycle-exclusive.test.ts
  • packages/core/src/index.ts
  • packages/core/src/settings-validation.ts
  • packages/core/src/task-store/settings-ops.ts
  • packages/core/src/types.ts
  • packages/dashboard/app/__tests__/settings-sections.test.tsx
  • packages/dashboard/app/components/settings/sections/WorktreesSection.tsx
  • packages/dashboard/src/routes/__tests__/register-settings-memory-worktrunk.test.ts
  • packages/dashboard/src/routes/register-settings-memory-routes.ts
  • packages/engine/src/__tests__/worktree-acquisition-pinned.test.ts
  • packages/engine/src/__tests__/worktree-pinning.test.ts
  • packages/engine/src/run-audit.ts
  • packages/engine/src/worktree-acquisition.ts
  • packages/engine/src/worktree-pinning.ts
  • packages/i18n/locales/en/app.json

Comment thread packages/engine/src/__tests__/worktree-acquisition-pinned.test.ts Outdated
Comment thread packages/engine/src/worktree-acquisition.ts
Comment thread packages/engine/src/worktree-acquisition.ts
- worktree-acquisition.ts pinnedWorktreeBranchMatches: reserve `false` for a
  proven branch mismatch. A transient `git worktree list` failure yields an empty
  enumeration (the helper swallows internally), which previously read as
  "foreign branch" and destructively reclaimed a valid warm worktree. Now throws
  (fail-safe) so acquisition retries with a fresh probe instead.
- worktree-acquisition.ts warm reuse: persist worktree+branch when adopting an
  orphaned pinned dir (task.worktree null) so a successful acquisition never
  leaves the task unassigned; idempotent when the cache was already correct.
- register-settings-memory-routes.ts: map the store's mutual-exclusion backstop
  error to 400 (not 500) for edge cases the route pre-check misses (e.g. a
  null-clear resolving to a conflicting fallback); dedupe the error-message read.
- worktree-acquisition-pinned.test.ts: FNXC heading on the test-seam comment;
  cover orphan-adoption metadata persistence and the fail-safe probe throw.
- worktrunk route test: cover the 400 backstop mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gsxdsm

gsxdsm commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Outside diff range commentpackages/dashboard/src/routes/register-settings-memory-routes.ts (register-settings catch block): "Map the backstop conflict error to a 400 status. ... because this catch block does not recognize the 'mutually exclusive' error string, it will return a 500 Internal Server Error instead of a clean 400."

Addressed in c47497352: the catch block now classifies the store-level mutual-exclusion backstop error ("mutually exclusive") as a 400 alongside the existing modelPresets / provider+modelId checks, and I folded the repeated err.message/String(err) reads into a single normalized errorMessage as suggested. Added a route test asserting the backstop error maps to 400 (not 500).

@gsxdsm
gsxdsm merged commit edc6413 into main Jul 16, 2026
7 checks passed
@gsxdsm
gsxdsm deleted the feature/task-pinned-worktrees branch July 16, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant