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
59 changes: 56 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Orchestrate complex, long-running coding tasks to an ephemeral cloud environment
- [Agent Workflow](./examples/agent/README.md)
- [Webhook Integration](./examples/webhook/README.md)
- [GitHub Actions](./examples/github-actions/README.md)
- [Gitpatch Review](./examples/gitpatch-review/README.md)

## Send work to a Cloud based session

Expand Down
91 changes: 91 additions & 0 deletions packages/core/examples/gitpatch-review/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# GitPatch Review Example (CLI)

This example demonstrates how to use Jules' session GitPatch to review and analyze code generated by a Jules coding agent against the context of a GitHub repository.

It is structured as a CLI using [citty](https://github.com/unjs/citty) and follows the **Typed Service Contract** (Spec & Handler) pattern to clearly separate argument parsing, schema validation, and business logic execution.

## Overview

The CLI orchestrates two sequential sessions using the Jules SDK:

1. **Generation Session**: Takes a user-provided prompt (e.g., instructing an agent to write poor code), producing a Git patch with changes.
2. **Review Session**: Takes the resulting Git patch generated from the first session and creates a *new* session, passing the patch string as context to instruct the agent to review and analyze the code against standard coding practices.

This demonstrates common workflows such as:
- Structuring Agent CLIs for deterministic and predictable behavior.
- Using `session.stream()` to output real-time CLI feedback.
- Extracting raw `gitPatch` strings or parsed `ChangeSet` structures directly from the `session.snapshot()`.

## Prerequisites

- Node.js or Bun installed.
- A Jules API Key. Set it using:
```bash
export JULES_API_KEY=<your-api-key>
```

## Running the Example

You can run this example using `bun` (recommended) or via Node/TSX.

### Basic Usage

```bash
bun run index.ts -r "owner/repo" -b "main" -p "Write a badly formatted hello world function in Python"
```

### CLI Arguments

| Argument | Alias | Required | Default | Description |
| :--- | :--- | :--- | :--- | :--- |
| `repository` | `-r` | **Yes** | | The target GitHub repository (e.g. `davideast/dataprompt`) |
| `prompt` | `-p` | **Yes** | | The prompt to generate the code change |
| `baseBranch` | `-b` | No | `main` | The base branch of the repository |
| `--json` | | No | `false` | Output the final result or error as structured JSON. Useful for agents or piping outputs. |

### Example Output

```text
Starting code generation session for davideast/dataprompt...
Code Generation Session ID: jules:session:12345
[Code Gen] Planning changes
[Code Gen] Generated plan with 1 steps.
[Code Gen] Editing code
[Code Gen Agent]: I've created the requested bad code.

--- Extracted Patch Content ---
--- a/bad_code.py
+++ b/bad_code.py
@@ -0,0 +1,2 @@
+def hw():
+ print("hello")
-------------------------------

Starting review session...
Review Session ID: jules:session:67890
[Review] Analyzing changes
[Review Agent]: This code lacks typing, has bad indentation, and uses a poor function name.

=======================================
REVIEW COMPLETE
=======================================

This code lacks typing, has bad indentation, and uses a poor function name. I recommend renaming it to 'hello_world' and fixing the indentation.
```

### JSON Output

When run with the `--json` flag, all stdout/stderr progress logs are suppressed or piped differently, and the final output is a structured JSON response (following the Result pattern).

```bash
bun run index.ts -r "owner/repo" -p "Write bad code" --json
```

```json
{
"codeGenSessionId": "jules:session:12345",
"reviewSessionId": "jules:session:67890",
"gitPatchStr": "...",
"reviewMessage": "This code lacks typing..."
}
```
104 changes: 104 additions & 0 deletions packages/core/examples/gitpatch-review/e2e-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { execa } from 'execa';
import { z } from 'zod';
import { ReviewSuccess } from './src/spec.js';

/**
* End-to-End Test for the GitPatch Review CLI
*
* This script invokes the CLI as a separate process to verify that:
* 1. The CLI can authenticate with the Jules API (using JULES_API_KEY).
* 2. It successfully starts and streams two consecutive sessions.
* 3. When the `--json` flag is provided, the final `stdout` is exclusively
* a valid JSON payload matching the `ReviewSuccess` schema.
* 4. Progress logs are successfully piped to `stderr` and don't corrupt the JSON.
*/
async function runE2E() {
const apiKey = process.env.JULES_API_KEY;

if (!apiKey) {
console.error('❌ E2E Test Failed: JULES_API_KEY environment variable is missing.');
process.exit(1);
}

console.log('🚀 Starting GitPatch Review CLI E2E Test...\n');

try {
// We use execa to easily spawn the CLI, capture stdout/stderr separately,
// and provide a timeout. The target repo here is arbitrary but must be valid.
const subprocess = execa('bun', [
'run',
'index.ts',
'-r',
'davideast/dataprompt',
'-b',
'main',
'-p',
'Write a Python function that adds two numbers, but name it very badly, use no types, and mess up the indentation.',
'--json',
], {
env: { JULES_API_KEY: apiKey },
timeout: 900000, // 15 minute timeout for two LLM sessions
cwd: import.meta.dir // Ensure we run relative to this e2e script
});

// Pipe stderr to our current console so we can watch the progress logs live
if (subprocess.stderr) {
subprocess.stderr.pipe(process.stderr);
}

const { stdout, exitCode } = await subprocess;

console.log('\n\n✅ CLI process exited with code:', exitCode);

if (exitCode !== 0) {
console.error('❌ E2E Test Failed: CLI exited with a non-zero status code.');
process.exit(1);
}

console.log('--- Raw CLI JSON Output (stdout) ---');
console.log(stdout);
console.log('------------------------------------\n');

// Parse and validate the stdout output against our expected Zod schema
const parsedJson = JSON.parse(stdout);
const validationResult = ReviewSuccess.safeParse({ success: true, data: parsedJson });

if (!validationResult.success) {
console.error('❌ E2E Test Failed: The JSON output did not match the expected schema.');
console.error(validationResult.error.format());
process.exit(1);
}

const { data } = validationResult.data;

console.log('✅ Validation Passed: Output is valid JSON.');
console.log(`- Code Gen Session ID: ${data.codeGenSessionId}`);
console.log(`- Review Session ID: ${data.reviewSessionId}`);

if (data.gitPatchStr && data.gitPatchStr.length > 0) {
console.log(`- Git Patch Extracted: YES (${data.gitPatchStr.split('\\n').length} lines)`);
} else {
console.error('❌ E2E Test Failed: No Git Patch string was found in the output.');
process.exit(1);
}

if (data.reviewMessage && data.reviewMessage.length > 0) {
console.log(`- Review Message Generated: YES`);
} else {
console.error('❌ E2E Test Failed: No Review Message was found in the output.');
process.exit(1);
}

console.log('\n🎉 E2E Test Completed Successfully!');

} catch (error: any) {
console.error('\n❌ E2E Test Failed with an exception:');
if (error.shortMessage) {
console.error(error.shortMessage); // execa formatting
}
console.error(error.message);
process.exit(1);
}
}

runE2E();
82 changes: 82 additions & 0 deletions packages/core/examples/gitpatch-review/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { defineCommand, runMain } from 'citty';
import { ReviewHandler } from './src/handler.js';
import { ReviewInputSchema } from './src/spec.js';

const main = defineCommand({
meta: {
name: 'jules-gitpatch-review',
version: '1.0.0',
description: 'Use Jules to review generated code patches against GitHub repo context.',
},
args: {
repository: {
type: 'string',
description: 'The target GitHub repository (e.g. owner/repo)',
required: true,
alias: 'r',
},
baseBranch: {
type: 'string',
description: 'The base branch of the repository',
default: 'main',
alias: 'b',
},
prompt: {
type: 'string',
description: 'The prompt to generate the code change',
required: true,
alias: 'p',
},
json: {
type: 'boolean',
description: 'Output the final result as JSON',
default: false,
},
},
async run({ args }) {
// 1. Validate Input (Parse, don't validate)
const inputResult = ReviewInputSchema.safeParse({
repository: args.repository,
baseBranch: args.baseBranch,
prompt: args.prompt,
json: args.json,
});

if (!inputResult.success) {
console.error('Invalid arguments provided:');
console.error(inputResult.error.format());
process.exit(1);
}

// 2. Instantiate the Handler
const handler = new ReviewHandler();

// 3. Execute Business Logic
const result = await handler.execute(inputResult.data);

// 4. Handle Results Deterministically
if (!result.success) {
if (args.json) {
console.error(JSON.stringify(result.error, null, 2));
} else {
console.error(`\n[ERROR] ${result.error.code}: ${result.error.message}`);
if (result.error.suggestion) {
console.error(`Suggestion: ${result.error.suggestion}`);
}
}
process.exit(1);
}

// 5. Output Success State
if (args.json) {
console.log(JSON.stringify(result.data, null, 2));
} else {
console.log('\n=======================================');
console.log(' REVIEW COMPLETE');
console.log('=======================================\n');
console.log(result.data.reviewMessage);
}
},
});

runMain(main);
20 changes: 20 additions & 0 deletions packages/core/examples/gitpatch-review/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "jules-gitpatch-review-example",
"version": "1.0.0",
"description": "Example demonstrating how to use Jules' session GitPatch to review and analyze generated code",
"main": "index.ts",
"type": "module",
"scripts": {
"start": "bun run index.ts",
"test:e2e": "bun run e2e-test.ts"
},
"dependencies": {
"@google/jules-sdk": "workspace:*",
"citty": "^0.1.6",
"zod": "^3.23.0"
},
"devDependencies": {
"bun-types": "^1.1.8",
"execa": "^9.6.1"
}
}
Loading
Loading