Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 70 additions & 128 deletions packages/merge/README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,22 @@
# Jules Merge

Detect and surface merge conflicts between a coding agent's changes and the base branchbefore or during CI.
Reconcile overlapping PR changes from parallel AI agentsscan, resolve, push, merge.

## Check for conflicts in CI
## Workflow

```bash
npx @google/jules-merge check-conflicts \
--repo your-org/your-repo \
--pr 42 \
--sha abc123
```

When a merge has already been attempted, `check-conflicts` reads conflict markers from the filesystem and returns structured JSON with the affected files, their conflict markers, and a task directive that tells the agent exactly what to resolve.

## Check for conflicts proactively

```bash
npx @google/jules-merge check-conflicts \
--session 7439826373470093109 \
--repo your-org/your-repo
1. scan — detect overlapping files and build the reconciliation manifest
2. get-contents — fetch file versions (base, main, pr:<N>) for each hot zone
3. stage-resolution — write resolved content for each conflicted file
4. status — confirm all files resolved (ready: true, pending is empty)
5. push — create the multi-parent reconciliation commit and PR
6. merge — merge the reconciliation PR using a merge commit
```

This queries the Jules SDK for the session's changed files and compares them against recent commits on the base branch. If files overlap, it returns the remote file content (`remoteShadowContent`) so the agent can resolve conflicts without needing `git pull`.

## Generate a CI workflow
## Quick Start

```bash
npx @google/jules-merge init
```

Writes `.github/workflows/jules-merge-check.yml` to your repo. The workflow runs on every pull request: it attempts a merge, and if conflicts exist, runs `check-conflicts` to produce structured output that Jules can act on.

```bash
npx @google/jules-merge init --base-branch develop --force
npx @google/jules-merge scan --json '{"prs":[10,11],"repo":"owner/repo"}'
```

## Installation
Expand All @@ -41,154 +25,112 @@ npx @google/jules-merge init --base-branch develop --force
npm i @google/jules-merge
```

For session-based checks, set authentication:
## Authentication

Uses the same auth pattern as Fleet. The CLI resolves auth internally — no external decode steps.

**GitHub App (recommended):**

```
JULES_API_KEY Required for session mode.
GITHUB_TOKEN Required. GitHub PAT with repo access.
FLEET_APP_ID App ID
FLEET_APP_PRIVATE_KEY_BASE64 Base64-encoded private key (canonical)
FLEET_APP_INSTALLATION_ID Installation ID
```

Or use GitHub App authentication:
Legacy names (`GITHUB_APP_*`) are accepted with a deprecation warning.

**Token (fallback):**

```
GITHUB_APP_ID App ID
GITHUB_APP_PRIVATE_KEY_BASE64 Base64-encoded private key
GITHUB_APP_INSTALLATION_ID Installation ID
GITHUB_TOKEN or GH_TOKEN
```

## CLI Reference

### `jules-merge check-conflicts`
All commands support `--json <payload>` for agent-first usage.

Detect merge conflicts. Mode is inferred from the arguments provided.
### `jules-merge scan`

```
jules-merge check-conflicts [options]
Scan PRs for overlapping file changes and build the reconciliation manifest.

Session mode (proactive):
--session <id> Jules session ID
--repo <owner/repo>
--base <branch> Base branch (default: main)

Git mode (CI failure):
--pr <number> Pull request number
--sha <sha> Failing commit SHA
--repo <owner/repo>
```bash
jules-merge scan --json '{"prs":[10,11],"repo":"owner/repo","base":"main"}'
jules-merge scan --prs 10,11 --repo owner/repo --base main
```

**Session mode** queries the Jules SDK for changed files and compares them against remote commits. Returns `remoteShadowContent` for each conflicting file.
### `jules-merge get-contents`

**Git mode** reads `git status` for unmerged files and extracts conflict markers. Returns a `taskDirective` with resolution instructions.
Fetch file content from base, main, or a specific PR.

### `jules-merge init`
```bash
jules-merge get-contents --json '{"filePath":"src/config.ts","source":"pr:10","repo":"owner/repo"}'
```

Generate a GitHub Actions workflow for automated conflict detection.
### `jules-merge stage-resolution`

```
jules-merge init [options]
Stage a resolved file for the reconciliation commit.

Options:
--output-dir <dir> Directory to write into (default: .)
--workflow-name <name> Filename without .yml (default: jules-merge-check)
--base-branch <branch> Branch to check against (default: main)
--force Overwrite existing file
```bash
jules-merge stage-resolution --json '{"filePath":"src/config.ts","parents":["main","10","11"],"content":"resolved content"}'
```

## Programmatic API
### `jules-merge status`

All handlers are exported for use in scripts, CI pipelines, or other packages.
Show reconciliation manifest status.

```ts
import {
SessionCheckHandler,
GitCheckHandler,
InitHandler,
} from '@google/jules-merge';
```bash
jules-merge status
```

### `SessionCheckHandler`

Compares a Jules session's changed files against remote commits on the base branch.
### `jules-merge push`

```ts
const handler = new SessionCheckHandler(octokit, julesClient);
const result = await handler.execute({
sessionId: '7439826373470093109',
repo: 'your-org/your-repo',
base: 'main',
});
Create the multi-parent reconciliation commit and PR.

if (result.success && result.data.status === 'conflict') {
for (const conflict of result.data.conflicts) {
console.log(`${conflict.filePath}: ${conflict.conflictReason}`);
console.log(conflict.remoteShadowContent);
}
}
```bash
jules-merge push --json '{"branch":"reconcile/batch","message":"Reconcile PRs","repo":"owner/repo"}'
```

Returns `{ status: 'clean' | 'conflict', conflicts: [...] }` on success. Each conflict includes `filePath`, `conflictReason`, and `remoteShadowContent`.

### `GitCheckHandler`
Supports `--mergeStrategy sequential` (default, enables GitHub auto-close) or `octopus`.

Reads conflict markers from the local filesystem after a failed merge.
### `jules-merge merge`

```ts
const handler = new GitCheckHandler();
const result = await handler.execute({
repo: 'your-org/your-repo',
pullRequestNumber: 42,
failingCommitSha: 'abc123',
});
Merge the reconciliation PR. Always uses merge commit — never squash or rebase.

if (result.success) {
console.log(result.data.taskDirective);
for (const file of result.data.affectedFiles) {
console.log(`${file.filePath}: ${file.gitConflictMarkers}`);
}
}
```bash
jules-merge merge --json '{"pr":999,"repo":"owner/repo"}'
```

Returns `{ taskDirective, priority, affectedFiles: [...] }` on success. Each file includes `filePath`, `baseCommitSha`, and `gitConflictMarkers`.

### `InitHandler`
### `jules-merge schema`

Generates a GitHub Actions workflow file.
Print JSON schema for command inputs/outputs.

```ts
const handler = new InitHandler();
const result = await handler.execute({
outputDir: '.',
workflowName: 'jules-merge-check',
baseBranch: 'main',
force: false,
});

if (result.success) {
console.log(`Created: ${result.data.filePath}`);
}
```bash
jules-merge schema scan
jules-merge schema --all
```

Returns `{ filePath, content }` on success.

### `buildWorkflowYaml`

Generate the workflow YAML string without writing to disk.
## Programmatic API

```ts
import { buildWorkflowYaml } from '@google/jules-merge';
import {
scanHandler,
getContentsHandler,
stageResolutionHandler,
statusHandler,
pushHandler,
mergeHandler,
createMergeOctokit,
} from '@google/jules-merge';

const yaml = buildWorkflowYaml({
workflowName: 'merge-check',
baseBranch: 'main',
});
const octokit = createMergeOctokit();
const scan = await scanHandler(octokit, { prs: [10, 11], repo: 'owner/repo' });
```

## MCP Server
All handlers take an Octokit instance as the first argument (dependency injection).

The package exposes an MCP server with two tools:
## MCP Server

- **`check_conflicts`** — Detects merge conflicts (session or git mode)
- **`init_workflow`** — Generates a CI workflow file
7 tools: `scan_fleet`, `get_file_contents`, `stage_resolution`, `get_status`, `push_reconciliation`, `merge_reconciliation`, `get_schema`.

```bash
jules-merge mcp
Expand Down
71 changes: 71 additions & 0 deletions packages/merge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: jules-merge
---

# jules-merge Agent Skill

## Workflow Order

Always execute commands in this sequence:

1. `scan` — detect conflicts and build the manifest
2. `get-contents` — fetch file versions (base, main, pr:<N>) for each hot zone
3. `stage-resolution` — write resolved content for each conflicted file
4. `status` — confirm all files resolved (`ready: true`, `pending` is empty)
5. `push` — create the multi-parent reconciliation commit and PR
6. `merge` — merge the reconciliation PR using a merge commit

## Command Reference

### `scan`
- Required: `prs` (array of PR numbers), `repo` (owner/repo)
- Optional: `base` (branch name, default: main), `includeClean`

### `get-contents`
- `source` values: `"base"` | `"main"` | `"pr:<N>"` (e.g. `"pr:42"`)
- Always check `totalLines` in the response — if it exceeds your context budget, surface to the user rather than attempting to process the file
- `"base"` = the common ancestor commit (merge base), not main

### `stage-resolution`
- `parents` format: `"main,<prNumber>,<prNumber>"` — always start with `"main"`, followed by the PR numbers (as strings) that touch the file
- Example: `["main", "10", "11"]`
- Either `content` (inline string) or `fromFile` (local path) must be provided
- Use `--dry-run` to validate without writing to the manifest

### `push`
- `mergeStrategy` values: `"sequential"` (default) | `"octopus"`
- `sequential`: creates N 2-parent merge commits in a chain — required for GitHub auto-close
- `octopus`: creates a single N-parent commit — use for non-GitHub platforms or atomic history
- Check `warnings` in the output — if `"BASE_SHA_MISMATCH"` is present, re-run `scan` before merging
- `--dry-run` is safe to call at any time; it validates without writing
- `push` is idempotent: calling it twice on the same branch reuses the existing PR
- When using `sequential`, the output includes `mergeChain` — an array of `{ commitSha, parents, prId }` per step

### `merge`
- Always uses merge commit strategy — never squash or rebase
- This preserves the ancestry chain that auto-closes fleet PRs via GitHub's "closes" detection

## Exit Codes

| Code | Meaning | Action |
|------|---------|--------|
| `0` | Success | Continue |
| `1` | Recoverable conflict | Surface to user or re-scan |
| `2` | Hard error | Abort and surface to user |

## Key Invariants

- **Merge strategy**: always `merge` (not squash/rebase) — squash breaks the ancestry chain that closes fleet PRs
- **Push merge strategy**: default `sequential` creates 2-parent chain for GitHub compatibility; use `octopus` only for non-GitHub platforms
- **parents format**: `["main", "<prN>", "<prN>"]` — the string `"main"` is always first, followed by PR numbers as strings
- **Scan before push**: if `push` returns `warnings: ["BASE_SHA_MISMATCH"]`, re-run `scan` to refresh the base SHA before proceeding
- **Context window**: check `totalLines` on every `get-contents` response; if a file is too large for your context budget, surface to the user rather than processing it
- **Idempotency**: `scan` overwrites the manifest; `push` reuses an existing open PR on the same branch
- **No pending files**: `push` will throw if `status.pending` is non-empty — resolve all hot zones first

## Schema Introspection

```
jules-merge schema <command> # input/output schema for one command
jules-merge schema --all # all schemas at once
```
2 changes: 1 addition & 1 deletion packages/merge/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ const shared = {
format: 'esm' as const,
root: './src',
external: [
'@google/jules-sdk',
'@octokit/auth-app',
'@octokit/rest',
'@modelcontextprotocol/sdk',
'citty',
'zod',
'zod-to-json-schema',
],
outdir: './dist',
naming: '[dir]/[name].mjs',
Expand Down
6 changes: 3 additions & 3 deletions packages/merge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@google/jules-merge",
"version": "0.0.3",
"type": "module",
"description": "Predictive conflict detection for parallel AI agents",
"description": "Reconcile overlapping PR changes from parallel AI agents",
"types": "./dist/merge/src/index.d.ts",
"bin": {
"jules-merge": "./dist/cli/index.mjs"
Expand Down Expand Up @@ -37,11 +37,11 @@
"access": "public"
},
"dependencies": {
"@google/jules-sdk": "^0.1.0",
"@octokit/auth-app": "^8.2.0",
"@octokit/rest": "^21.0.0",
"citty": "^0.1.6",
"zod": "^3.25.0"
"zod": "^3.25.0",
"zod-to-json-schema": "^3.24.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.25.1"
Expand Down
Loading
Loading