Skip to content

feat(core): extract sync handler#168

Open
galligan wants to merge 1 commit into
fire-7/pr-handlersfrom
fire-8/sync-handler
Open

feat(core): extract sync handler#168
galligan wants to merge 1 commit into
fire-7/pr-handlersfrom
fire-8/sync-handler

Conversation

@galligan

@galligan galligan commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Extract syncHandler unifying sync orchestration from CLI and MCP
  • Accept scopes option for selective sync
  • Progress reporting via SyncProgressCallback in handler input (FIRE-8)

Test plan

  • bun run check passes
  • bun test passes (288 tests)

🤘🏻 In-collaboration-with: Claude Code

Greptile Summary

Extracts syncHandler to unify sync orchestration between CLI and MCP, moving auth, plugin detection, and cache clearing logic into the core handler.

Critical Issue Found:
The PR claims to "Accept scopes option for selective sync" but the implementation is incomplete:

  • SyncInput interface missing scopes parameter
  • syncHandler never passes scope to syncRepo(), defaulting to "open" only
  • Both CLI and MCP iterate through scopes but don't pass them to the handler
  • This breaks the ability to sync closed/merged PRs when using --closed flag

Other Changes:

  • Removed direct GitHubClient, detectAuth, clearRepo, and Graphite plugin logic from CLI/MCP
  • Added progress callback support via SyncProgressCallback
  • Handler returns per-repo results with SyncRepoResult[]
  • Comprehensive test coverage for validation, auth, clear flag, and progress callbacks (but missing scope tests)

Confidence Score: 1/5

  • This PR has a critical logic bug that breaks core functionality
  • The scopes feature is advertised but completely non-functional - syncHandler always defaults to "open" scope, breaking closed/merged PR syncing. While tests pass, they don't cover scope functionality
  • Pay close attention to packages/core/src/handlers/sync.ts - requires scopes parameter support and proper iteration logic

Important Files Changed

Filename Overview
packages/core/src/handlers/sync.ts Extracted sync handler with critical bug: missing scopes support despite PR claiming to accept scopes option
apps/cli/src/commands/sync.ts Refactored to use syncHandler but fails to pass scope parameter, breaking multi-scope sync functionality
apps/mcp/src/index.ts Updated to use syncHandler with incorrect iteration structure - iterates scopes but doesn't pass to handler
packages/core/tests/handlers/sync.test.ts Tests for syncHandler covering validation, auth, clear flag, and progress callbacks - missing scope functionality tests

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[CLI/MCP calls syncHandler] --> B[Resolve repos from input/config/detection]
    B --> C[Validate repo formats]
    C --> D{Clear flag set?}
    D -->|Yes| E[Clear cache for each repo]
    D -->|No| F[Authenticate with GitHub]
    E --> F
    F --> G[Create GitHubClient]
    G --> H[Detect Graphite plugin availability]
    H --> I[Iterate through repos]
    I --> J[Fire onProgress start event]
    J --> K[Call syncRepo with plugins]
    K --> L{Sync successful?}
    L -->|Yes| M[Fire onProgress done event]
    L -->|No| N[Fire onProgress error event]
    M --> O{More repos?}
    N --> O
    O -->|Yes| I
    O -->|No| P{All failed?}
    P -->|Yes| Q[Return error]
    P -->|No| R[Return aggregate results]
Loading

Fix All in Claude Code Fix All in Codex

Last reviewed commit: 1558176

@linear

linear Bot commented Feb 22, 2026

Copy link
Copy Markdown

Adds `syncHandler` in `packages/core/src/handlers/sync.ts` that unifies
repo resolution, authentication, optional cache clearing, and `syncRepo()`
invocation behind the standard `Handler<SyncInput, SyncOutput>` contract.

- Resolves repos from input.repos → config.repos → auto-detection
- Validates repo format before touching any state
- Clears cache (via clearRepo) before auth, so --clear works even if auth fails
- Detects Graphite availability and applies the plugin automatically
- Fires onProgress callbacks for start/done/error per repo
- Returns aggregate SyncOutput with per-repo results, totalEntries, duration

Wires CLI (sync.ts) and MCP (performSync) to call syncHandler.
Removes duplicate auth + GitHubClient + Graphite setup code from both transports.
Removes unused parseSince import and resolveGraphiteEnabled function from MCP.

10 new tests covering validation, auth failure, clear flag, progress callbacks,
and repo resolution from config.

🤘🏻 In-collaboration-with: [Claude Code](https://claude.com/claude-code)
@galligan galligan force-pushed the fire-8/sync-handler branch from fc63d67 to 1558176 Compare February 22, 2026 03:51
@galligan galligan changed the title feat(core): extract syncHandler from CLI/MCP sync orchestration feat(core): extract sync handler Feb 22, 2026
@galligan galligan marked this pull request as ready for review February 22, 2026 04:07

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

6 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +33 to +47
export interface SyncInput {
/**
* Explicit repos to sync (owner/repo format).
* If not provided, uses config.repos + auto-detection.
*/
repos?: string[] | undefined;
/** Clear cached entries for each repo before syncing. */
clear?: boolean | undefined;
/** Force full sync (ignore incremental cursors). */
full?: boolean | undefined;
/** Maximum number of PRs to fetch per repo (defaults to config value). */
maxPrs?: number | undefined;
/** Progress callback for reporting sync status per repo. */
onProgress?: SyncProgressCallback | undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

missing scopes parameter - PR summary claims "Accept scopes option for selective sync" but SyncInput has no scopes field

Suggested change
export interface SyncInput {
/**
* Explicit repos to sync (owner/repo format).
* If not provided, uses config.repos + auto-detection.
*/
repos?: string[] | undefined;
/** Clear cached entries for each repo before syncing. */
clear?: boolean | undefined;
/** Force full sync (ignore incremental cursors). */
full?: boolean | undefined;
/** Maximum number of PRs to fetch per repo (defaults to config value). */
maxPrs?: number | undefined;
/** Progress callback for reporting sync status per repo. */
onProgress?: SyncProgressCallback | undefined;
}
export interface SyncInput {
/**
* Explicit repos to sync (owner/repo format).
* If not provided, uses config.repos + auto-detection.
*/
repos?: string[] | undefined;
/** Clear cached entries for each repo before syncing. */
clear?: boolean | undefined;
/** Force full sync (ignore incremental cursors). */
full?: boolean | undefined;
/** Which PR scopes to sync (default: ["open"]). */
scopes?: SyncScope[] | undefined;
/** Maximum number of PRs to fetch per repo (defaults to config value). */
maxPrs?: number | undefined;
/** Progress callback for reporting sync status per repo. */
onProgress?: SyncProgressCallback | undefined;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/handlers/sync.ts
Line: 33-47

Comment:
missing `scopes` parameter - PR summary claims "Accept `scopes` option for selective sync" but `SyncInput` has no scopes field

```suggestion
export interface SyncInput {
  /**
   * Explicit repos to sync (owner/repo format).
   * If not provided, uses config.repos + auto-detection.
   */
  repos?: string[] | undefined;
  /** Clear cached entries for each repo before syncing. */
  clear?: boolean | undefined;
  /** Force full sync (ignore incremental cursors). */
  full?: boolean | undefined;
  /** Which PR scopes to sync (default: ["open"]). */
  scopes?: SyncScope[] | undefined;
  /** Maximum number of PRs to fetch per repo (defaults to config value). */
  maxPrs?: number | undefined;
  /** Progress callback for reporting sync status per repo. */
  onProgress?: SyncProgressCallback | undefined;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +184 to +195
for (const repo of repos) {
input.onProgress?.(repo, "start");

try {
// Graphite enrichment only applies to the detected local repo
const useGraphite = graphiteAvailable;
const plugins = useGraphite ? [graphitePlugin] : [];

const syncResult = await syncRepo(client, repo, {
...(input.full && { full: true }),
plugins,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

scope parameter not passed to syncRepo() - will always default to "open" scope, breaking closed/merged PR sync

needs to iterate scopes from input.scopes and pass each to syncRepo():

const scopes = input.scopes ?? (['open'] satisfies SyncScope[]);

for (const scope of scopes) {
  for (const repo of repos) {
    // ... existing repo iteration logic
    const syncResult = await syncRepo(client, repo, {
      ...(input.full && { full: true }),
      scope,  // <-- add this
      plugins,
    });
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/handlers/sync.ts
Line: 184-195

Comment:
`scope` parameter not passed to `syncRepo()` - will always default to "open" scope, breaking closed/merged PR sync

needs to iterate scopes from `input.scopes` and pass each to `syncRepo()`:

```typescript
const scopes = input.scopes ?? (['open'] satisfies SyncScope[]);

for (const scope of scopes) {
  for (const repo of repos) {
    // ... existing repo iteration logic
    const syncResult = await syncRepo(client, repo, {
      ...(input.full && { full: true }),
      scope,  // <-- add this
      plugins,
    });
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines 211 to +226
for (const scope of scopes) {
const ctx: SyncContext = {
const ctx: SyncDisplayContext = {
config,
repo,
db,
outputJson,
isFullSync,
scope,
};
const startTime = Date.now();
const spinner = createSpinner(ctx, options);

try {
const result = await syncRepo(client, repo, {
const result = await syncHandler(
{
repos: [repo],
clear: options.clear,
full: isFullSync,
scope,
plugins,
});
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CLI iterates scopes but never passes them to syncHandler - all syncs will use default "open" scope

the handler needs to accept scopes:

Suggested change
for (const scope of scopes) {
const ctx: SyncContext = {
const ctx: SyncDisplayContext = {
config,
repo,
db,
outputJson,
isFullSync,
scope,
};
const startTime = Date.now();
const spinner = createSpinner(ctx, options);
try {
const result = await syncRepo(client, repo, {
const result = await syncHandler(
{
repos: [repo],
clear: options.clear,
full: isFullSync,
scope,
plugins,
});
},
// Delegate auth + plugin resolution + sync to handler
for (const scope of scopes) {
const ctx: SyncDisplayContext = {
config,
repo,
outputJson,
isFullSync,
scope,
};
const spinner = createSpinner(ctx, options);
const result = await syncHandler(
{
repos: [repo],
clear: options.clear,
full: isFullSync,
scopes: [scope],
},
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/cli/src/commands/sync.ts
Line: 211-226

Comment:
CLI iterates scopes but never passes them to `syncHandler` - all syncs will use default "open" scope

the handler needs to accept scopes:

```suggestion
  // Delegate auth + plugin resolution + sync to handler
  for (const scope of scopes) {
    const ctx: SyncDisplayContext = {
      config,
      repo,
      outputJson,
      isFullSync,
      scope,
    };
    const spinner = createSpinner(ctx, options);

    const result = await syncHandler(
      {
        repos: [repo],
        clear: options.clear,
        full: isFullSync,
        scopes: [scope],
      },
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment thread apps/mcp/src/index.ts
Comment on lines +586 to +591
for (const scope of scopes) {
const syncResult = await syncHandler(
{
repos,
...(options.full && { full: true }),
...(options.since && {
since: (() => {
const sinceResult = parseSince(options.since!);
if (sinceResult.isErr()) {
throw new Error(sinceResult.error.message);
}
return sinceResult.value;
})(),
}),
scope,
plugins: useGraphite ? [graphitePlugin] : [],
});
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

scopes iteration but never passes scope to syncHandler - handler will ignore the loop and always sync "open" scope

fix by passing scopes to handler:

Suggested change
for (const scope of scopes) {
const syncResult = await syncHandler(
{
repos,
...(options.full && { full: true }),
...(options.since && {
since: (() => {
const sinceResult = parseSince(options.since!);
if (sinceResult.isErr()) {
throw new Error(sinceResult.error.message);
}
return sinceResult.value;
})(),
}),
scope,
plugins: useGraphite ? [graphitePlugin] : [],
});
},
for (const scope of scopes) {
const syncResult = await syncHandler(
{
repos,
scopes: [scope],
...(options.full && { full: true }),
},
{ config, db, logger: silentLogger }
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mcp/src/index.ts
Line: 586-591

Comment:
`scopes` iteration but never passes scope to `syncHandler` - handler will ignore the loop and always sync "open" scope

fix by passing scopes to handler:

```suggestion
  for (const scope of scopes) {
    const syncResult = await syncHandler(
      {
        repos,
        scopes: [scope],
        ...(options.full && { full: true }),
      },
      { config, db, logger: silentLogger }
    );
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

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

Copy link
Copy Markdown

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: 155817621e

ℹ️ 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".

Comment on lines +192 to +194
const syncResult = await syncRepo(client, repo, {
...(input.full && { full: true }),
plugins,

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 Pass requested sync scope into syncRepo

syncHandler invokes syncRepo without a scope, so syncRepo always falls back to open; callers in CLI/MCP are looping over requested scopes externally and then reporting that loop scope, which makes --closed (or open+closed runs) fetch the wrong dataset while still labeling results as closed. This breaks selective sync semantics and stores sync metadata under the wrong scope.

Useful? React with 👍 / 👎.

const result = await syncHandler(
{
repos: [repo],
clear: options.clear,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear cache only once across multi-scope sync

This clear flag is passed on every iteration of the scope loop, and syncHandler clears before syncing; when users run fw sync --open --closed --clear, the second iteration clears out data written by the first iteration, so the final cache no longer contains both scopes. Clear should happen once before the loop (or only on the first scope) to preserve combined results.

Useful? React with 👍 / 👎.

Comment on lines +189 to +190
const useGraphite = graphiteAvailable;
const plugins = useGraphite ? [graphitePlugin] : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate Graphite enrichment to the detected repo

Graphite enrichment is enabled for every repo whenever Graphite is available locally, but multi-repo syncs include repos unrelated to the local checkout; because the Graphite plugin matches stack membership by PR number, non-detected repos can be incorrectly tagged with local stack metadata when PR numbers overlap. This should stay scoped to the detected local repo as before.

Useful? React with 👍 / 👎.

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