Skip to content

feat: implement pre-cognitive agent with shadow worktrees#6

Open
OctavianTocan wants to merge 3 commits intomainfrom
precognitive-agent
Open

feat: implement pre-cognitive agent with shadow worktrees#6
OctavianTocan wants to merge 3 commits intomainfrom
precognitive-agent

Conversation

@OctavianTocan
Copy link
Copy Markdown
Owner

@OctavianTocan OctavianTocan commented Mar 18, 2026

Summary

  • Implemented background daemon that provisions invisible git worktrees (.raft-shadow).
  • Integrated Claude-based auto-fix generation natively into the background sync loop.
  • Transformed the PR List into a Decision Queue: auto-fixes are boosted to the absolute highest urgency.
  • Unified the Spacebar as the deterministically optimal action key (view/apply AI fixes, or squash & merge).

Summary by Sourcery

Replace GitHub CLI shell integration with direct GitHub API usage, add a local SQLite-backed cache and background daemon for AI-generated fixes, and extend the UI with split-branch visualization and richer PR browsing controls.

New Features:

  • Introduce a background daemon that analyzes PRs, generates AI-based fixes in shadow git worktrees, and surfaces them in the PR list.
  • Add a split command and supporting split-state model to visualize and navigate split branch topologies from the CLI UI.
  • Persist PR metadata, details, panel data, and generated fixes in a local SQLite database for faster startup and offline-friendly behavior.
  • Group file diffs by semantic intent (source, tests, config, boilerplate) with optional boilerplate collapsing in the files panel, plus keyboard toggles.
  • Show skeleton loading states in the preview panel while PR body, comments, code, or files data are fetched.

Enhancements:

  • Refactor GitHub integration to use REST and GraphQL APIs via an auth-token-based fetch layer instead of shelling out to the gh CLI for most operations.
  • Improve PR list performance and ergonomics with deferred search, urgency-aware sorting that prioritizes PRs with auto-fixes, and memoized row rendering.
  • Extend the status view and keyboard bindings so Space acts as the primary action key for applying fixes, merging, jumping to reviews, or copying PR URLs based on lifecycle state.
  • Enhance PR detail fetching with batched GraphQL queries, pagination for files, and more robust repository detection from git remotes.
  • Augment the panel and cache layers to hydrate from and write through to the SQLite cache, keeping UI state responsive while background fetches and AI work occur.

Tests:

  • Update GitHub parsing tests and integration tests to match the new GitHub API data shapes and repository choices, and adjust cache tests to use unique keys to avoid cross-test interference.

- Replaced 140+ `gh` CLI subprocess calls with native `fetch` via GitHub API
- Added in-memory OAuth token caching via `gh auth token`
- Added local SQLite persistence using `bun:sqlite` for zero-latency PR lists
- Optimized React rendering cascade with `useDeferredValue` and `React.memo`
- Implemented `PanelSkeleton` for smooth, tab-aware loading transitions
- Batched GraphQL PR details queries into chunks to prevent rate limits
Copilot AI review requested due to automatic review settings March 18, 2026 17:30
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 18, 2026

Reviewer's Guide

Refactors GitHub integration away from the gh CLI to direct REST/GraphQL calls, adds a local SQLite-backed cache and background daemon that generates/apply AI fixes via shadow git worktrees, upgrades the ls UI to prioritize auto-fixable PRs and unify Space as the main action key, and introduces split-branch visualization, semantic diff grouping, and richer panel skeletons/perf tweaks.

Sequence diagram for background daemon generating AI fixes in shadow worktrees

sequenceDiagram
  participant Ls as LsCommand
  participant D as runBackgroundDaemon
  participant Cache as PRCache
  participant DB as SQLite
  participant GH as GitHub_APIs
  participant Shadow as shadow_worktree
  participant AI as generateFix
  participant Repo as RepoRoot

  Ls->>D: runBackgroundDaemon(prs, detailsMap, onFixGenerated)
  activate D
  D->>Repo: getRepoRoot()
  Note over D,Repo: If no repoRoot, daemon exits

  loop for each PR
    D->>Cache: details = detailsMap.get(pr.url)
    alt has details
      D->>D: state = detectPRState(pr, details)
      alt state == FIX_REVIEW
        D->>GH: fetchReviewThreads(repo, prNumber)
        GH-->>D: unresolvedThreads
        loop each unresolved thread
          D->>DB: existingFixes = getGeneratedFixes(pr.url)
          alt no existing fix for thread
            D->>Shadow: wtDir = prepareShadowWorktree(repoRoot, pr.number)
            Shadow-->>D: wtDir
            D->>AI: fix = generateFix(thread, wtDir)
            alt fix generated
              D->>DB: cacheGeneratedFix(pr.url, thread.id, fix)
              D->>Ls: onFixGenerated(pr.url, thread.id)
            end
          end
        end
      else state == FIX_CI
        D->>DB: existingFixes = getGeneratedFixes(pr.url)
        alt no existing CI fix
          D->>Shadow: wtDir = prepareShadowWorktree(repoRoot, pr.number)
          Shadow-->>D: wtDir
          D->>AI: generateCIFix(pr, wtDir)
          AI-->>D: ciFix or null
          alt ciFix
            D->>DB: cacheGeneratedFix(pr.url, "ci", ciFix)
            D->>Ls: onFixGenerated(pr.url, "ci")
          end
        end
      end
    end
  end
  deactivate D
Loading

Sequence diagram for Space key applying an AI fix via shadow worktree

sequenceDiagram
  actor User
  participant Ls as LsCommand
  participant DB as SQLite
  participant Fix as applyFix
  participant Shadow as shadow_worktree
  participant Git as git
  participant GH as GitHub_remote
  participant Repo as Repo
  participant FS as FileSystem
  participant Clipboard

  User->>Ls: press Space on selected PR
  Ls->>DB: selectedFixes = getGeneratedFixes(pr.url)
  alt has auto-fix
    Ls->>Ls: showFlash("Applying AI fix...")
    Ls->>Repo: root = getRepoRoot()
    alt root found
      Ls->>Fix: applyFix(fix, pr, root)
      activate Fix
      Fix->>Shadow: wtDir = prepareShadowWorktree(root, pr.number)
      Shadow-->>Fix: wtDir
      Fix->>FS: write modifiedContent to wtDir/path
      Fix->>Git: git add path (cwd=wtDir)
      Fix->>Git: git commit -m "fix: address review comment..." (cwd=wtDir)
      Fix->>Git: git push origin raft-shadow/pr-n:headRefName (cwd=wtDir)
      Git-->>GH: update PR branch
      Fix->>Shadow: cleanupShadowWorktree(root, pr.number)
      deactivate Fix
      Ls->>DB: clearGeneratedFix(pr.url, fix.threadId)
      Ls->>Ls: increment fixUpdateTrigger
      Ls->>Ls: showFlash("Fix pushed to PR branch!")
    else no repo root
      Ls->>Ls: showFlash("Error: Not in a local git repo.")
    end
  else no auto-fix
    alt lifecycle MERGE_NOW
      User->>Ls: Space
      Ls->>Git: runGhMerge(repo, prNumber)
    else lifecycle FIX_REVIEW
      Ls->>Ls: open panel code tab
    else lifecycle PING_REVIEWERS
      Ls->>Clipboard: copy PR URL
    else
      Ls->>Ls: move selection to next PR
    end
  end
Loading

ER diagram for new SQLite cache and generated fixes tables

erDiagram
  pull_requests {
    TEXT url PK
    JSON data
    DATETIME updated_at
  }

  pr_details {
    TEXT url PK
    JSON data
    DATETIME updated_at
  }

  pr_panel_data {
    TEXT url PK
    JSON data
    DATETIME updated_at
  }

  generated_fixes {
    TEXT id PK
    TEXT pr_url
    TEXT thread_id
    JSON data
    DATETIME updated_at
  }

  pull_requests ||--|| pr_details : same_pr_url
  pull_requests ||--|| pr_panel_data : same_pr_url
  pull_requests ||--o{ generated_fixes : has_fixes
Loading

Class diagram for caching, daemon, shadow worktrees, and split state

classDiagram
  class PRCache {
    -details Map~string, PRDetails~
    -panelData Map~string, PRPanelData~
    +getDetails(url string) PRDetails
    +setDetails(url string, data PRDetails) void
    +hasDetails(url string) boolean
    +getPanelData(url string) PRPanelData
    +setPanelData(url string, data PRPanelData) void
    +hasPanelData(url string) boolean
  }

  class DBModule {
    +db Database
    +getCachedPRs() PullRequest[]
    +cachePRs(prs PullRequest[]) void
    +getCachedPRDetails(url string) PRDetails
    +cachePRDetails(url string, details PRDetails) void
    +getCachedPRPanelData(url string) PRPanelData
    +cachePRPanelData(url string, panelData PRPanelData) void
    +getGeneratedFixes(prUrl string) any[]
    +cacheGeneratedFix(prUrl string, threadId string, fix any) void
    +clearGeneratedFix(prUrl string, threadId string) void
  }

  class DaemonModule {
    +runBackgroundDaemon(prs PullRequest[], detailsMap Map~string, PRDetails~, onFixGenerated function) void
  }

  class ShadowModule {
    +prepareShadowWorktree(repoRoot string, prNumber number) string
    +cleanupShadowWorktree(repoRoot string, prNumber number) void
  }

  class AIFixModule {
    +applyFix(fix ProposedFix, pr PullRequest, repoRoot string) void
    +generateFix(thread ReviewThread, wtDir string) ProposedFix
  }

  class GithubAuth {
    -cachedToken string
    +getGithubToken() Promise~string~
  }

  class GithubLib {
    +fetchGh(endpoint string, options RequestInit) any
    +fetchGhGraphql(query string, variables any) any
    +parseSearchResults(items any[]) PullRequest[]
    +fetchOpenPRs(author string, onProgress function) PullRequest[]
    +fetchRepoPRs(repo string) PullRequest[]
    +updatePRTitle(repo string, prNumber number, title string) void
    +fetchPRDetails(repo string, prNumber number) PRDetails
    +batchFetchPRDetails(prs any[]) Map~string, PRDetails~
    +fetchPRPanelData(repo string, prNumber number) PRPanelData
    +fetchReviewThreads(repo string, prNumber number) ReviewThread[]
    +resolveReviewThread(threadId string) void
    +fetchCIStatus(repo string, ref string) string
    +fetchHasConflicts(repo string, prNumber number) boolean
    +getCurrentRepo() string
  }

  class SplitEntry {
    +number number
    +name string
    +branch string
    +files string[]
    +lines number
    +dependsOn number[]
    +prNumber number
    +prUrl string
    +status SplitEntryStatus
  }

  class SplitState {
    +version number
    +originalBranch string
    +targetBranch string
    +strategy string
    +createdAt string
    +status SplitPhase
    +topology string
    +splits SplitEntry[]
  }

  class SplitStateModule {
    +readSplitState(repoRoot string) SplitState
    +writeSplitState(repoRoot string, state SplitState) void
    +formatSplitTopology(state SplitState) string[]
  }

  class SplitCommand {
    +SplitCommand(props SplitCommandProps)
  }

  class SplitCommandProps {
    +repo string
  }

  class DiffUtils {
    +getFileIntent(filename string) FileIntent
  }

  class FileIntent

  PRCache --> DBModule : uses
  DaemonModule --> PRCache : reads_writes
  DaemonModule --> ShadowModule : manages_worktrees
  DaemonModule --> AIFixModule : generateFix
  DaemonModule --> DBModule : cacheGeneratedFix

  AIFixModule --> ShadowModule : prepare_cleanup
  AIFixModule --> GithubLib : uses_PR_metadata

  GithubLib --> GithubAuth : uses_token

  SplitCommand --> SplitStateModule : read_split_state
  SplitStateModule --> SplitState : constructs
  SplitState --> SplitEntry : aggregates

  DiffUtils ..> FileIntent : defines
Loading

File-Level Changes

Change Details Files
Replace gh CLI shelling with first-class GitHub REST/GraphQL helpers and adapt all PR/data-fetching flows.
  • Introduce fetchGh/fetchGhGraphql helpers using getGithubToken and standardize error handling.
  • Change parseSearchResults to consume REST search.items and map to PullRequest with repo/user fields from REST shape.
  • Rewrite fetchOpenPRs/fetchAllAccountPRs/fetchRepoPRs to use GitHub REST search/issues and pulls endpoints instead of account switching via gh.
  • Implement batchFetchPRDetails using a GraphQL batch query and make fetchPRDetails a thin wrapper over it.
  • Port fetchPRPanelData to REST+GraphQL, including paginated file fetching and hydration of comments/threads.
  • Simplify fetchReviewThreads, resolveReviewThread, fetchCIStatus, fetchHasConflicts to use new helpers and remove multi-account runner plumbing.
  • Update tests that depended on gh CLI shapes and specific repos to the new REST-based shapes and repo (cli/cli).
src/lib/github.ts
src/lib/auth.ts
src/lib/git-utils.ts
src/lib/__tests__/github.test.ts
src/__tests__/integration.test.ts
Add a Bun/SQLite-backed local persistence layer for PRs, details, panel data, and generated AI fixes and integrate it with caches.
  • Create db.ts to open ~/.config/raft/raft.sqlite and define tables for pull_requests, pr_details, pr_panel_data, and generated_fixes.
  • Provide helpers to cache and retrieve PR lists, PRDetails, PRPanelData, and per-PR generated fixes, including transactional PR list replacement.
  • Wire PRCache to read-through/write-through to SQLite so details and panel data survive process restarts.
  • Expose helpers getCachedPRs/cachePRs/getGeneratedFixes/cacheGeneratedFix/clearGeneratedFix for higher-level code.
src/lib/db.ts
src/lib/cache.ts
Introduce background daemon and shadow worktrees to pre-generate AI fixes and apply them safely to PR branches.
  • Add shadow.ts utilities to create and tear down .raft-shadow worktrees per PR, fetching pull/N/head into raft-shadow/pr-{number}.
  • Extend ai-fix.applyFix to operate in a shadow worktree, commit the fix, and push raft-shadow/pr-{n} to the PR headRefName, then clean up.
  • Implement daemon.ts to iterate over PRs, inspect lifecycle state, and generate fixes for unresolved review threads (and stub CI fixes), caching them in SQLite.
  • Hook runBackgroundDaemon into ls so it runs when details are available and triggers UI refresh via a fixUpdateTrigger.
  • Ensure .raft-shadow gets its own .gitignore to keep worktrees hidden from the main repo.
src/lib/ai-fix.ts
src/lib/shadow.ts
src/lib/daemon.ts
src/lib/db.ts
src/commands/ls.tsx
Transform the ls command into a decision queue that prioritizes AI auto-fixes, adds offline caching, and defines Space as the universal next-action key.
  • Seed allPRs from cached PRs on mount, then refresh via fetchOpenPRs and persist back to SQLite; keep errors non-fatal if cache exists.
  • Introduce deferredSearchQuery via useDeferredValue to smooth search filtering performance.
  • Compute an urgencyMap that bumps PRs with generated fixes to an urgency of 110, and sort "attention" mode primarily by this urgency then age.
  • Split filteredPRs from sortedPRs so grouping and selection operate on the sorted view, and adjust scroll/selection logic to use a stable scrollOffsetRef.
  • Auto-start runBackgroundDaemon after rebuilding detailsMap and re-render when fixes arrive via fixUpdateTrigger.
  • Define a universal Space handler: apply the first AI fix if present (using getRepoRoot/applyFix/clearGeneratedFix), otherwise merge, open review, or copy URL depending on lifecycle state, or advance to next PR.
  • Memoize PRRow with React.memo and compute open/draft counts with useMemo for incremental perf wins, plus minor selection/jump fixes (age sort timestamps, bounds checks).
src/commands/ls.tsx
src/components/status-view.tsx
src/components/pr-table.tsx
src/lib/git-utils.ts
src/lib/db.ts
Enhance the files panel with semantic grouping (Source/Tests/Config/Boilerplate) and optional boilerplate collapsing, using new diff utilities.
  • Add getFileIntent and FileIntent types to diff-utils to classify filenames as Boilerplate/Config/Tests/Source Code based on paths and extensions.
  • Refactor PanelFiles to group FileDiffs by intent in a fixed order and render per-group headers plus per-file diffs via a shared renderFile helper.
  • Introduce a B keybinding to toggle showing boilerplate; when hidden, show a compact summary line with counts and +/- stats for Boilerplate.
  • Remove the old collapseDiffRegions helper and related markers, simplifying diff-utils to only naming and intent/view-mode logic.
src/components/panel-files.tsx
src/lib/diff-utils.ts
Improve panel UX with skeleton placeholders and tighter React state typing for panel controls.
  • Add PanelSkeleton to skeleton.tsx with different placeholder layouts for body/comments/code/files tabs.
  • Update PreviewPanel to show PanelSkeleton beneath the spinner when loading and panelData is not yet available.
  • Tighten usePanel types by making splitRatio/panelFullscreen state setters explicit React.Dispatch<SetStateAction<...>> types.
src/components/skeleton.tsx
src/components/preview-panel.tsx
src/hooks/usePanel.ts
Introduce a new split command and split-state model to visualize split-branch topology and status for pre-PR branch splitting workflows.
  • Add split-state.ts defining SplitState/SplitEntry models, JSON persistence to .raft-split.json, and a formatter that renders a tree-like topology from dependency edges.
  • Create SplitCommand that reads split state from the current repo, renders topology and a list of split entries with status badges, and supports j/k navigation plus Enter/open and c/copy.
  • Wire split into the CLI parser and command switch and update help text accordingly.
src/lib/split-state.ts
src/commands/split.tsx
src/index.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

Warning

Rate limit exceeded

@OctavianTocan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 27 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb67f9ae-4e43-4412-8085-99f0dcced7e0

📥 Commits

Reviewing files that changed from the base of the PR and between a2a9d59 and 1497779.

📒 Files selected for processing (22)
  • src/__tests__/integration.test.ts
  • src/commands/ls.tsx
  • src/commands/split.tsx
  • src/components/panel-files.tsx
  • src/components/pr-table.tsx
  • src/components/preview-panel.tsx
  • src/components/skeleton.tsx
  • src/components/status-view.tsx
  • src/hooks/usePanel.ts
  • src/index.tsx
  • src/lib/__tests__/cache.test.ts
  • src/lib/__tests__/github.test.ts
  • src/lib/ai-fix.ts
  • src/lib/auth.ts
  • src/lib/cache.ts
  • src/lib/daemon.ts
  • src/lib/db.ts
  • src/lib/diff-utils.ts
  • src/lib/git-utils.ts
  • src/lib/github.ts
  • src/lib/shadow.ts
  • src/lib/split-state.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch precognitive-agent
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The background daemon and fix-generation paths create shadow worktrees via prepareShadowWorktree but never clean them up on completion or error (only applyFix calls cleanupShadowWorktree); consider ensuring every daemon path (including generateFix/generateCIFix) pairs worktree creation with cleanup to avoid accumulating stale worktrees/branches.
  • There are now two separate implementations of getRepoRoot (in git-utils.ts and inline in split.tsx); it would be cleaner and less error-prone to reuse the shared helper instead of spawning git rev-parse in multiple places.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The background daemon and fix-generation paths create shadow worktrees via `prepareShadowWorktree` but never clean them up on completion or error (only `applyFix` calls `cleanupShadowWorktree`); consider ensuring every daemon path (including `generateFix`/`generateCIFix`) pairs worktree creation with cleanup to avoid accumulating stale worktrees/branches.
- There are now two separate implementations of `getRepoRoot` (in `git-utils.ts` and inline in `split.tsx`); it would be cleaner and less error-prone to reuse the shared helper instead of spawning `git rev-parse` in multiple places.

## Individual Comments

### Comment 1
<location path="src/lib/daemon.ts" line_range="44-45" />
<code_context>
+            
+            // Generate fix
+            try {
+              const wtDir = await prepareShadowWorktree(repoRoot, pr.number);
+              const fix = await generateFix(thread, wtDir);
+              if (fix) {
+                cacheGeneratedFix(pr.url, thread.id, fix);
</code_context>
<issue_to_address>
**issue (bug_risk):** Shadow worktrees created by the daemon are never cleaned up, which can leak worktrees/branches over time.

This code calls `prepareShadowWorktree` for every fix generation but never calls `cleanupShadowWorktree`, so `raft-shadow/pr-*` branches and worktrees will accumulate on disk as more PRs/threads are processed. Since `applyFix` already has its own prepare/cleanup cycle, consider either reusing a shared worktree instead of creating new branches each time, or explicitly invoking `cleanupShadowWorktree` when a PR’s processing (success or failure) is complete.
</issue_to_address>

### Comment 2
<location path="src/lib/ai-fix.ts" line_range="137" />
<code_context>
+export async function applyFix(fix: ProposedFix, pr: PullRequest, repoRoot: string): Promise<void> {
+  const wtDir = await prepareShadowWorktree(repoRoot, pr.number)
+  
+  const filePath = `${wtDir}/${fix.path}`
   await Bun.write(filePath, fix.modifiedContent)
+  
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Prefer path.join over manual string concatenation for building file paths.

The hard-coded `/` assumes POSIX paths and may break on platforms like Windows or if `fix.path` includes leading slashes. Using `path.join(wtDir, fix.path)` (or an equivalent helper) will be more robust and portable.

Suggested implementation:

```typescript
import * as path from "node:path"
import { safeSpawn, buildCleanEnv } from "./process"

```

```typescript
  const wtDir = await prepareShadowWorktree(repoRoot, pr.number)
  const filePath = path.join(wtDir, fix.path)

```
</issue_to_address>

### Comment 3
<location path="src/commands/split.tsx" line_range="11-18" />
<code_context>
+  repo?: string
+}
+
+async function getRepoRoot(): Promise<string | null> {
+  const proc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
+    stdout: "pipe",
+    stderr: "pipe",
+  })
+  const stdout = await new Response(proc.stdout).text()
+  const code = await proc.exited
+  return code === 0 ? stdout.trim() : null
+}
+
</code_context>
<issue_to_address>
**suggestion:** Avoid duplicating repo-root discovery logic that already exists in git-utils.

Please use the existing `getRepoRoot` helper in `lib/git-utils.ts` instead of reimplementing this. It already wraps this command via `safeSpawn` and provides consistent environment and error handling.

Suggested implementation:

```typescript
import { useState, useEffect, useCallback } from "react"
import { useKeyboard, useRenderer } from "@opentui/react"
import { Spinner } from "../components/spinner"
import { readSplitState, formatSplitTopology } from "../lib/split-state"
import { getRepoRoot } from "../lib/git-utils"

```

```typescript
interface SplitCommandProps {
  repo?: string
}

```

If the `getRepoRoot` exported from `../lib/git-utils` has a different signature (e.g. accepts a `cwd` parameter or returns a non-nullable string), adjust the call sites in this file accordingly to pass the appropriate arguments and handle its return type consistently with the rest of the codebase.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +44 to +45
const wtDir = await prepareShadowWorktree(repoRoot, pr.number);
const fix = await generateFix(thread, wtDir);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Shadow worktrees created by the daemon are never cleaned up, which can leak worktrees/branches over time.

This code calls prepareShadowWorktree for every fix generation but never calls cleanupShadowWorktree, so raft-shadow/pr-* branches and worktrees will accumulate on disk as more PRs/threads are processed. Since applyFix already has its own prepare/cleanup cycle, consider either reusing a shared worktree instead of creating new branches each time, or explicitly invoking cleanupShadowWorktree when a PR’s processing (success or failure) is complete.

export async function applyFix(fix: ProposedFix, pr: PullRequest, repoRoot: string): Promise<void> {
const wtDir = await prepareShadowWorktree(repoRoot, pr.number)

const filePath = `${wtDir}/${fix.path}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Prefer path.join over manual string concatenation for building file paths.

The hard-coded / assumes POSIX paths and may break on platforms like Windows or if fix.path includes leading slashes. Using path.join(wtDir, fix.path) (or an equivalent helper) will be more robust and portable.

Suggested implementation:

import * as path from "node:path"
import { safeSpawn, buildCleanEnv } from "./process"
  const wtDir = await prepareShadowWorktree(repoRoot, pr.number)
  const filePath = path.join(wtDir, fix.path)

Comment on lines +11 to +18
async function getRepoRoot(): Promise<string | null> {
const proc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
stdout: "pipe",
stderr: "pipe",
})
const stdout = await new Response(proc.stdout).text()
const code = await proc.exited
return code === 0 ? stdout.trim() : null
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Avoid duplicating repo-root discovery logic that already exists in git-utils.

Please use the existing getRepoRoot helper in lib/git-utils.ts instead of reimplementing this. It already wraps this command via safeSpawn and provides consistent environment and error handling.

Suggested implementation:

import { useState, useEffect, useCallback } from "react"
import { useKeyboard, useRenderer } from "@opentui/react"
import { Spinner } from "../components/spinner"
import { readSplitState, formatSplitTopology } from "../lib/split-state"
import { getRepoRoot } from "../lib/git-utils"
interface SplitCommandProps {
  repo?: string
}

If the getRepoRoot exported from ../lib/git-utils has a different signature (e.g. accepts a cwd parameter or returns a non-nullable string), adjust the call sites in this file accordingly to pass the appropriate arguments and handle its return type consistently with the rest of the codebase.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1497779ff9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const CHUNK_SIZE = 20;
for (let i = 0; i < prs.length; i += CHUNK_SIZE) {
const chunk = prs.slice(i, i + CHUNK_SIZE);
let query = "query {\\n";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Build GraphQL batch query with real newlines

The query prefix is initialized as "query {\\n", which inserts a literal backslash-n sequence into the GraphQL document rather than an actual newline. GraphQL parsers treat \ outside strings as invalid syntax, so batchFetchPRDetails can fail for every chunk; because the error is caught and only logged later, callers silently get empty detail maps and lose lifecycle/urgency data.

Useful? React with 👍 / 👎.

Comment on lines +142 to +143
await safeSpawn(["git", "add", fix.path], { cwd: wtDir })
await safeSpawn(["git", "commit", "-m", commitMsg], { cwd: wtDir })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Propagate git command failures when applying fixes

applyFix awaits safeSpawn but never checks exitCode; safeSpawn returns non-zero exits instead of throwing. If git add/commit/push fails (e.g., empty commit, auth failure, protected branch), this function still resolves and the UI reports success, which can falsely mark a fix as applied and clear queued fixes even though nothing was pushed.

Useful? React with 👍 / 👎.

// Assuming 'origin' is the remote name. In a more robust setup we might need to parse remote.
// But for now, we'll fetch from origin or try to fetch the PR head directly.
try {
await safeSpawn(["git", "fetch", "origin", `pull/${prNumber}/head:${branchName}`], { cwd: repoRoot });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate git fetch/worktree exit status in shadow setup

prepareShadowWorktree assumes safeSpawn throws on command failure and wraps calls in try/catch, but non-zero exits are returned in-band. A failed git fetch or git worktree add can therefore be treated as success, causing the daemon to proceed with a non-existent or stale worktree path and generate no fixes without surfacing the real git error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a background “pre-cognitive” workflow that provisions shadow git worktrees for PRs, generates AI fixes in the background, and integrates persistent caching + UI affordances to prioritize and apply those fixes. It also refactors GitHub integration away from gh JSON output toward direct REST/GraphQL API calls using an auth token sourced from gh auth token.

Changes:

  • Add shadow worktree management + a background daemon that generates and caches AI fixes.
  • Replace most gh CLI-based GitHub data fetching with REST/GraphQL fetch helpers + token auth.
  • Add SQLite-backed caching and update UI/commands (Decision Queue behavior, split view, panel skeletons, grouped files, Space action).

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/lib/split-state.ts Adds split-branch state model + formatting helpers
src/lib/shadow.ts Implements shadow worktree fetch/add/remove helpers
src/lib/github.ts Replaces gh CLI execution with REST/GraphQL fetch helpers
src/lib/git-utils.ts Adds getRepoRoot() helper
src/lib/diff-utils.ts Adds file “intent” classification for grouping
src/lib/db.ts Adds SQLite persistence for PRs/details/panel data/fixes
src/lib/daemon.ts Adds background daemon to generate/cached fixes
src/lib/cache.ts Extends PRCache to read/write through SQLite
src/lib/auth.ts Adds GitHub token retrieval via gh auth token
src/lib/ai-fix.ts Applies fixes via shadow worktree commit + push
src/lib/tests/github.test.ts Updates parseSearchResults tests for REST shape
src/lib/tests/cache.test.ts Adjusts test to account for persistent cache
src/index.tsx Adds split command entry
src/hooks/usePanel.ts Tightens state setter typings
src/components/status-view.tsx Surfaces “Auto-Fix Ready” and Space hint
src/components/skeleton.tsx Adds panel skeleton content
src/components/preview-panel.tsx Uses skeleton while loading panel data
src/components/pr-table.tsx Memoizes PRRow
src/components/panel-files.tsx Groups files by intent + boilerplate toggle
src/commands/split.tsx New split state UI command
src/commands/ls.tsx Adds caching, daemon, fix prioritization, Space action
src/tests/integration.test.ts Updates target repo for integration tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +39 to +41
export function writeSplitState(repoRoot: string, state: SplitState): void {
const path = `${repoRoot}/.raft-split.json`
Bun.write(path, JSON.stringify(state, null, 2) + "\n")
Comment on lines +7 to +11
// Initialize database in ~/.config/raft/raft.sqlite
const configDir = join(homedir(), ".config", "raft");
mkdirSync(configDir, { recursive: true });

const dbPath = join(configDir, "raft.sqlite");
Comment on lines +6 to +20

interface SplitCommandProps {
repo?: string
}

async function getRepoRoot(): Promise<string | null> {
const proc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
stdout: "pipe",
stderr: "pipe",
})
const stdout = await new Response(proc.stdout).text()
const code = await proc.exited
return code === 0 ? stdout.trim() : null
}

// We can get the PR's remote branch using the GitHub API or the PR metadata.
// The PR object has `headRefName` which is the remote branch name.
// Let's assume the remote is 'origin'.
const branchName = `raft-shadow/pr-${pr.number}`
Comment on lines +135 to +153
const wtDir = await prepareShadowWorktree(repoRoot, pr.number)

const filePath = `${wtDir}/${fix.path}`
await Bun.write(filePath, fix.modifiedContent)

const commitMsg = `fix: address review comment by @${fix.reviewer}\n\nComment: ${fix.comment}`

await safeSpawn(["git", "add", fix.path], { cwd: wtDir })
await safeSpawn(["git", "commit", "-m", commitMsg], { cwd: wtDir })

// The branch name is raft-shadow/pr-{number}
// We need to push this to the PR's actual remote branch.
// We can get the PR's remote branch using the GitHub API or the PR metadata.
// The PR object has `headRefName` which is the remote branch name.
// Let's assume the remote is 'origin'.
const branchName = `raft-shadow/pr-${pr.number}`
await safeSpawn(["git", "push", "origin", `${branchName}:${pr.headRefName}`], { cwd: wtDir })

await cleanupShadowWorktree(repoRoot, pr.number)
Comment on lines +87 to 90
onProgress?.("Fetching PRs...");
const json = await fetchGh("search/issues?q=is:pr+is:open+author:@me&per_page=100");
return parseSearchResults(json.items);
}
Comment on lines +13 to +16
// Ensure .raft-shadow exists and is ignored
try {
await Bun.write(join(shadowDir, ".gitignore"), "*\n");
} catch {}
Comment on lines +58 to +61
try {
const wtDir = await prepareShadowWorktree(repoRoot, pr.number);
const fix = await generateCIFix(pr, wtDir);
if (fix) {
Comment on lines +150 to +165
const urgencyMap = useMemo(() => {
const map = new Map<string, number>()
for (const pr of filteredPRs) {
const details = detailsMap.get(pr.url) ?? null
const state = detectPRState(pr, details)
let urgency = state.urgency

// Auto-fixes are the absolute highest priority (urgency 110)
if (getGeneratedFixes(pr.url).length > 0) {
urgency = 110
}

map.set(pr.url, urgency)
}
return map
}, [filteredPRs, detailsMap, fixUpdateTrigger])
Comment on lines +18 to +33
// Fetch the PR head
// Assuming 'origin' is the remote name. In a more robust setup we might need to parse remote.
// But for now, we'll fetch from origin or try to fetch the PR head directly.
try {
await safeSpawn(["git", "fetch", "origin", `pull/${prNumber}/head:${branchName}`], { cwd: repoRoot });
} catch (e) {
// If origin fails, try upstream or just generic fetch?
// Let's assume origin for now.
throw new Error(`Failed to fetch PR ${prNumber} head: ${e}`);
}

// Check if worktree already exists
const { stdout: wtList } = await safeSpawn(["git", "worktree", "list"], { cwd: repoRoot });
if (wtList.includes(wtDir)) {
// Worktree already exists, just reset it to the fetched branch
await safeSpawn(["git", "reset", "--hard", branchName], { cwd: wtDir });
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.

2 participants