From 4b4b8f5037cd24c48104ead5d107a0b69e1337a3 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:53:49 +0000 Subject: [PATCH 1/3] feat(sdk): Add GitPatch Review example Adds a new practical example to the Jules TypeScript SDK demonstrating how to use the SDK to generate code and review the resulting GitPatch against standard coding practices. Updates the main packages/core/README.md to include a link to the new example. Included a new package in packages/core/examples/gitpatch-review. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 26 ++- packages/core/README.md | 1 + .../core/examples/gitpatch-review/README.md | 71 ++++++++ .../core/examples/gitpatch-review/index.ts | 164 ++++++++++++++++++ .../examples/gitpatch-review/package.json | 15 ++ 5 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/gitpatch-review/README.md create mode 100644 packages/core/examples/gitpatch-review/index.ts create mode 100644 packages/core/examples/gitpatch-review/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..cd8fd04 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ }, "packages/core": { "name": "@google/jules-sdk", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76", @@ -65,6 +65,16 @@ "typescript": "^5.0.0", }, }, + "packages/core/examples/gitpatch-review": { + "name": "jules-gitpatch-review-example", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + }, + "devDependencies": { + "bun-types": "^1.1.8", + }, + }, "packages/core/examples/webhook": { "name": "webhook", "version": "1.0.0", @@ -79,7 +89,7 @@ }, "packages/fleet": { "name": "@google/jules-fleet", - "version": "0.0.1-experimental.31", + "version": "0.0.1-experimental.32", "bin": { "jules-fleet": "dist/cli/index.mjs", }, @@ -105,7 +115,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -773,6 +783,8 @@ "jules-github-actions-example": ["jules-github-actions-example@workspace:packages/core/examples/github-actions"], + "jules-gitpatch-review-example": ["jules-gitpatch-review-example@workspace:packages/core/examples/gitpatch-review"], + "jules-sdk-example": ["jules-sdk-example@workspace:examples/simple"], "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], @@ -1145,8 +1157,14 @@ "@google/jules-fleet/@google/jules-merge": ["@google/jules-merge@0.0.2", "", { "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" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.1" }, "optionalPeers": ["@modelcontextprotocol/sdk"], "bin": { "jules-merge": "dist/cli/index.mjs" } }, "sha512-VPpbdBt48AbmFByg5RGztv2sQPPJ5fFJARXTvX41lY/b9M+qdyZPp19+ay3qfxEHgj1EvnRMwf/HFOrMMXZ7vQ=="], + "@google/jules-fleet/@google/jules-sdk": ["@google/jules-sdk@0.1.0", "", { "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76" } }, "sha512-DBVhFOsLfWaVtO0miEeX+zQoazB55EYmK5/0VJSX/YHdeheZqNuPlKRV1vrnd9uSQAtU6ACRicQssMbnvJM6ng=="], + "@google/jules-fleet/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@google/jules-mcp/@google/jules-sdk": ["@google/jules-sdk@0.1.0", "", { "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76" } }, "sha512-DBVhFOsLfWaVtO0miEeX+zQoazB55EYmK5/0VJSX/YHdeheZqNuPlKRV1vrnd9uSQAtU6ACRicQssMbnvJM6ng=="], + + "@google/jules-merge/@google/jules-sdk": ["@google/jules-sdk@0.1.0", "", { "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76" } }, "sha512-DBVhFOsLfWaVtO0miEeX+zQoazB55EYmK5/0VJSX/YHdeheZqNuPlKRV1vrnd9uSQAtU6ACRicQssMbnvJM6ng=="], + "@microsoft/api-extractor/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -1251,6 +1269,8 @@ "@actions/github/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + "@google/jules-fleet/@google/jules-merge/@google/jules-sdk": ["@google/jules-sdk@workspace:packages/core"], + "@google/jules-fleet/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "@google/jules-fleet/glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], diff --git a/packages/core/README.md b/packages/core/README.md index c3d32e7..46c9b47 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -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 diff --git a/packages/core/examples/gitpatch-review/README.md b/packages/core/examples/gitpatch-review/README.md new file mode 100644 index 0000000..b1cee00 --- /dev/null +++ b/packages/core/examples/gitpatch-review/README.md @@ -0,0 +1,71 @@ +# GitPatch Review Example + +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. + +## Overview + +The example runs two sequential sessions using the Jules SDK: + +1. **Generation Session**: Instructs an agent to intentionally write poor code, producing a Git patch with terrible practices. +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: +- Checking if code generated by Jules sticks to original prompt goals. +- Determining if generated code adheres to repository standards. +- Extracting raw `gitPatch` strings or parsed `ChangeSet` structures from the SDK's abstraction layer. + +## Prerequisites + +- Node.js or Bun installed. +- A Jules API Key. Set it using: + ```bash + export JULES_API_KEY= + ``` + +## Running the Example + +You can run this example using `bun`, `tsx`, or `ts-node`: + +### Using Bun + +```bash +bun run index.ts +``` + +### Using Node.js and TSX + +If you don't have `bun` installed, you can run the example using `tsx`: + +```bash +npm install -g tsx +tsx index.ts +``` + +## Example Output + +```text +1. Starting a new session to generate code... +Code Generation Session ID: jules:session:12345 +Waiting for the code generation to complete... + +2. Retrieving GitPatch data from the session... +Found ChangeSet on session snapshot. + +--- Extracted Patch Content --- +File: bad_math.ts (Additions: 4, Deletions: 0) +------------------------------- + +3. Starting a new session to review the GitPatch... +Review Session ID: jules:session:67890 +Waiting for the review to complete... + +--- Review Agent Analysis --- +This code is terrible! Here are the issues... +1. The function is named 'a' and takes parameters 'x' and 'y', making it unreadable. +2. It lacks basic TypeScript type annotations. +3. The spacing is inconsistent. + +Recommendation: +Rename the function to 'addNumbers', add type hints, and fix formatting. +----------------------------- +``` diff --git a/packages/core/examples/gitpatch-review/index.ts b/packages/core/examples/gitpatch-review/index.ts new file mode 100644 index 0000000..aa7286e --- /dev/null +++ b/packages/core/examples/gitpatch-review/index.ts @@ -0,0 +1,164 @@ +import { jules } from '@google/jules-sdk'; + +/** + * GitPatch Review Example + * + * Demonstrates how to use the Jules SDK to generate a code change + * and then review its GitPatch content. This is useful for analyzing + * generated code before applying it, checking it against coding standards, + * or using it to drive further automation. + */ +async function main() { + if (!process.env.JULES_API_KEY) { + console.error('Error: JULES_API_KEY environment variable is not set.'); + console.error('Please set it using: export JULES_API_KEY="your-api-key"'); + process.exit(1); + } + + // Define the target repository and base branch + const source = { github: 'davideast/dataprompt', baseBranch: 'main' }; + + console.log('1. Starting a new session to generate code...'); + + try { + // Start a session to generate some changes. We instruct the agent to + // intentionally write bad code so we have something obvious to review. + const codeGenSession = await jules.session({ + prompt: `Create a new file called 'bad_math.ts' with a poorly implemented +function that adds two numbers together. Include no comments and terrible variable names.`, + source, + }); + + console.log(`Code Generation Session ID: ${codeGenSession.id}`); + console.log('Waiting for the code generation to complete...'); + + // Await the completion of the session + const genOutcome = await codeGenSession.result(); + + if (genOutcome.state !== 'completed' && genOutcome.state !== 'succeeded') { + console.error(`Code generation session failed or did not complete. State: ${genOutcome.state}`); + return; + } + + // Retrieve the activities to find the changeset artifact + console.log('\n2. Retrieving GitPatch data from the session...'); + const activities = await jules.select({ + from: 'activities', + where: { 'session.id': codeGenSession.id }, + order: 'desc', + }); + + let gitPatchStr = ''; + + // Iterate through activities to find a ChangeSet artifact + for (const activity of activities) { + if (activity.artifacts) { + for (const artifact of activity.artifacts) { + if (artifact.type === 'changeSet') { + // In the SDK, the underlying structure for ChangeSet includes the gitPatch. + // We can extract the unidiffPatch from the raw artifact data. + const parsed = artifact.parsed(); + // Often, you might want the raw patch string to send to another agent or tool. + // We'll reconstruct a simple patch string from the parsed diff for this example. + + for(const file of parsed.files) { + gitPatchStr += `--- a/${file.path}\n+++ b/${file.path}\n`; + for(const chunk of file.chunks) { + gitPatchStr += `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; + for(const change of chunk.changes) { + if(change.type === 'add') gitPatchStr += `+${change.content}\n`; + if(change.type === 'del') gitPatchStr += `-${change.content}\n`; + if(change.type === 'normal') gitPatchStr += ` ${change.content}\n`; + } + } + } + } + } + } + } + + if (!gitPatchStr) { + console.log('No GitPatch found. The agent might not have written any code.'); + // If no gitpatch was found in activities, sometimes the final state has it on the outcome snapshot. + const snapshot = codeGenSession.snapshot(); + const changeSet = snapshot?.changeSet(); + if(changeSet) { + console.log("Found ChangeSet on session snapshot."); + // Note: in a real scenario you would access the raw gitpatch string if the API exposes it directly, + // but for the SDK's abstraction, parsing the diff is the recommended way. + const parsed = changeSet.parsed(); + for(const file of parsed.files) { + gitPatchStr += `File: ${file.path} (Additions: ${file.additions}, Deletions: ${file.deletions})\n`; + } + } + } + + if(!gitPatchStr) { + console.log("Could not find any changes. Exiting."); + return; + } + + console.log('\n--- Extracted Patch Content ---'); + console.log(gitPatchStr); + console.log('-------------------------------\n'); + + console.log('3. Starting a new session to review the GitPatch...'); + + // Now, create a second session to review the patch generated by the first session. + // This demonstrates using Jules to evaluate code against coding standards. + const reviewSession = await jules.session({ + prompt: `Review the following code change patch and determine if it adheres to clean coding standards. +Provide a short summary of the issues found and how they should be fixed. + +## Git Patch +\`\`\`diff +${gitPatchStr} +\`\`\` +`, + // We can optionally attach the same source repo context if the review needs to know about other files + source + }); + + console.log(`Review Session ID: ${reviewSession.id}`); + console.log('Waiting for the review to complete...'); + + const reviewOutcome = await reviewSession.result(); + + if (reviewOutcome.state === 'completed' || reviewOutcome.state === 'succeeded') { + // Find the agent's review message + const reviewActivities = await jules.select({ + from: 'activities', + where: { type: 'agentMessaged', 'session.id': reviewSession.id }, + order: 'desc', + limit: 1, + }); + + if (reviewActivities.length > 0) { + console.log('\n--- Review Agent Analysis ---'); + console.log(reviewActivities[0].message); + console.log('-----------------------------'); + } else { + // Fallback if the agent wrote the review to a file instead of a message + const files = reviewOutcome.generatedFiles(); + if (files.size > 0) { + console.log('\n--- Review Agent Analysis (Files) ---'); + for (const [filename, content] of files.entries()) { + console.log(`\nFile: ${filename}`); + console.log(content.content); + } + console.log('-------------------------------------'); + } else { + console.log("The review agent completed but didn't leave a message or file."); + } + } + } else { + console.error(`Review session failed or did not complete. State: ${reviewOutcome.state}`); + } + + } catch (error) { + console.error('An error occurred during the process:', error); + } +} + +// Run the example +main(); diff --git a/packages/core/examples/gitpatch-review/package.json b/packages/core/examples/gitpatch-review/package.json new file mode 100644 index 0000000..f2863f2 --- /dev/null +++ b/packages/core/examples/gitpatch-review/package.json @@ -0,0 +1,15 @@ +{ + "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", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*" + }, + "devDependencies": { + "bun-types": "^1.1.8" + } +} From 73d4ded3a09a2970b960767031fc9b73c76c4ffd Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:14:15 +0000 Subject: [PATCH 2/3] feat(sdk): Add GitPatch Review CLI example using Typed Service Contract Adds a new practical example to the Jules TypeScript SDK demonstrating how to use the SDK to generate code and review the resulting GitPatch against standard coding practices. The example is built as an Agent-ready CLI using the `citty` framework, follows the Typed Service Contract pattern to separate schemas from business logic, streams activities back to the user, and uses the `session.snapshot()` to safely extract the GitPatch. Updates the main packages/core/README.md to include a link to the new example. Included a new package in packages/core/examples/gitpatch-review. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 2 + .../core/examples/gitpatch-review/README.md | 86 ++++--- .../core/examples/gitpatch-review/index.ts | 220 ++++++------------ .../examples/gitpatch-review/package.json | 5 +- .../examples/gitpatch-review/src/handler.ts | 194 +++++++++++++++ .../core/examples/gitpatch-review/src/spec.ts | 47 ++++ 6 files changed, 369 insertions(+), 185 deletions(-) create mode 100644 packages/core/examples/gitpatch-review/src/handler.ts create mode 100644 packages/core/examples/gitpatch-review/src/spec.ts diff --git a/bun.lock b/bun.lock index cd8fd04..e99d7a7 100644 --- a/bun.lock +++ b/bun.lock @@ -70,6 +70,8 @@ "version": "1.0.0", "dependencies": { "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "zod": "^3.23.0", }, "devDependencies": { "bun-types": "^1.1.8", diff --git a/packages/core/examples/gitpatch-review/README.md b/packages/core/examples/gitpatch-review/README.md index b1cee00..4364448 100644 --- a/packages/core/examples/gitpatch-review/README.md +++ b/packages/core/examples/gitpatch-review/README.md @@ -1,18 +1,20 @@ -# GitPatch Review Example +# 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 example runs two sequential sessions using the Jules SDK: +The CLI orchestrates two sequential sessions using the Jules SDK: -1. **Generation Session**: Instructs an agent to intentionally write poor code, producing a Git patch with terrible practices. +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: -- Checking if code generated by Jules sticks to original prompt goals. -- Determining if generated code adheres to repository standards. -- Extracting raw `gitPatch` strings or parsed `ChangeSet` structures from the SDK's abstraction layer. +- 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 @@ -24,48 +26,66 @@ This demonstrates common workflows such as: ## Running the Example -You can run this example using `bun`, `tsx`, or `ts-node`: +You can run this example using `bun` (recommended) or via Node/TSX. -### Using Bun +### Basic Usage ```bash -bun run index.ts +bun run index.ts -r "owner/repo" -b "main" -p "Write a badly formatted hello world function in Python" ``` -### Using Node.js and TSX - -If you don't have `bun` installed, you can run the example using `tsx`: +### CLI Arguments -```bash -npm install -g tsx -tsx index.ts -``` +| 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 +### Example Output ```text -1. Starting a new session to generate code... +Starting code generation session for davideast/dataprompt... Code Generation Session ID: jules:session:12345 -Waiting for the code generation to complete... - -2. Retrieving GitPatch data from the session... -Found ChangeSet on session snapshot. +[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 --- -File: bad_math.ts (Additions: 4, Deletions: 0) +--- a/bad_code.py ++++ b/bad_code.py +@@ -0,0 +1,2 @@ ++def hw(): ++ print("hello") ------------------------------- -3. Starting a new session to review the GitPatch... +Starting review session... Review Session ID: jules:session:67890 -Waiting for the review to complete... +[Review] Analyzing changes +[Review Agent]: This code lacks typing, has bad indentation, and uses a poor function name. + +======================================= + REVIEW COMPLETE +======================================= ---- Review Agent Analysis --- -This code is terrible! Here are the issues... -1. The function is named 'a' and takes parameters 'x' and 'y', making it unreadable. -2. It lacks basic TypeScript type annotations. -3. The spacing is inconsistent. +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 +``` -Recommendation: -Rename the function to 'addNumbers', add type hints, and fix formatting. ------------------------------ +```json +{ + "codeGenSessionId": "jules:session:12345", + "reviewSessionId": "jules:session:67890", + "gitPatchStr": "...", + "reviewMessage": "This code lacks typing..." +} ``` diff --git a/packages/core/examples/gitpatch-review/index.ts b/packages/core/examples/gitpatch-review/index.ts index aa7286e..72a6645 100644 --- a/packages/core/examples/gitpatch-review/index.ts +++ b/packages/core/examples/gitpatch-review/index.ts @@ -1,164 +1,82 @@ -import { jules } from '@google/jules-sdk'; - -/** - * GitPatch Review Example - * - * Demonstrates how to use the Jules SDK to generate a code change - * and then review its GitPatch content. This is useful for analyzing - * generated code before applying it, checking it against coding standards, - * or using it to drive further automation. - */ -async function main() { - if (!process.env.JULES_API_KEY) { - console.error('Error: JULES_API_KEY environment variable is not set.'); - console.error('Please set it using: export JULES_API_KEY="your-api-key"'); - process.exit(1); - } - - // Define the target repository and base branch - const source = { github: 'davideast/dataprompt', baseBranch: 'main' }; - - console.log('1. Starting a new session to generate code...'); - - try { - // Start a session to generate some changes. We instruct the agent to - // intentionally write bad code so we have something obvious to review. - const codeGenSession = await jules.session({ - prompt: `Create a new file called 'bad_math.ts' with a poorly implemented -function that adds two numbers together. Include no comments and terrible variable names.`, - source, +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, }); - console.log(`Code Generation Session ID: ${codeGenSession.id}`); - console.log('Waiting for the code generation to complete...'); - - // Await the completion of the session - const genOutcome = await codeGenSession.result(); - - if (genOutcome.state !== 'completed' && genOutcome.state !== 'succeeded') { - console.error(`Code generation session failed or did not complete. State: ${genOutcome.state}`); - return; + if (!inputResult.success) { + console.error('Invalid arguments provided:'); + console.error(inputResult.error.format()); + process.exit(1); } - // Retrieve the activities to find the changeset artifact - console.log('\n2. Retrieving GitPatch data from the session...'); - const activities = await jules.select({ - from: 'activities', - where: { 'session.id': codeGenSession.id }, - order: 'desc', - }); - - let gitPatchStr = ''; + // 2. Instantiate the Handler + const handler = new ReviewHandler(); - // Iterate through activities to find a ChangeSet artifact - for (const activity of activities) { - if (activity.artifacts) { - for (const artifact of activity.artifacts) { - if (artifact.type === 'changeSet') { - // In the SDK, the underlying structure for ChangeSet includes the gitPatch. - // We can extract the unidiffPatch from the raw artifact data. - const parsed = artifact.parsed(); - // Often, you might want the raw patch string to send to another agent or tool. - // We'll reconstruct a simple patch string from the parsed diff for this example. + // 3. Execute Business Logic + const result = await handler.execute(inputResult.data); - for(const file of parsed.files) { - gitPatchStr += `--- a/${file.path}\n+++ b/${file.path}\n`; - for(const chunk of file.chunks) { - gitPatchStr += `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; - for(const change of chunk.changes) { - if(change.type === 'add') gitPatchStr += `+${change.content}\n`; - if(change.type === 'del') gitPatchStr += `-${change.content}\n`; - if(change.type === 'normal') gitPatchStr += ` ${change.content}\n`; - } - } - } - } + // 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); } - if (!gitPatchStr) { - console.log('No GitPatch found. The agent might not have written any code.'); - // If no gitpatch was found in activities, sometimes the final state has it on the outcome snapshot. - const snapshot = codeGenSession.snapshot(); - const changeSet = snapshot?.changeSet(); - if(changeSet) { - console.log("Found ChangeSet on session snapshot."); - // Note: in a real scenario you would access the raw gitpatch string if the API exposes it directly, - // but for the SDK's abstraction, parsing the diff is the recommended way. - const parsed = changeSet.parsed(); - for(const file of parsed.files) { - gitPatchStr += `File: ${file.path} (Additions: ${file.additions}, Deletions: ${file.deletions})\n`; - } - } - } - - if(!gitPatchStr) { - console.log("Could not find any changes. Exiting."); - return; - } - - console.log('\n--- Extracted Patch Content ---'); - console.log(gitPatchStr); - console.log('-------------------------------\n'); - - console.log('3. Starting a new session to review the GitPatch...'); - - // Now, create a second session to review the patch generated by the first session. - // This demonstrates using Jules to evaluate code against coding standards. - const reviewSession = await jules.session({ - prompt: `Review the following code change patch and determine if it adheres to clean coding standards. -Provide a short summary of the issues found and how they should be fixed. - -## Git Patch -\`\`\`diff -${gitPatchStr} -\`\`\` -`, - // We can optionally attach the same source repo context if the review needs to know about other files - source - }); - - console.log(`Review Session ID: ${reviewSession.id}`); - console.log('Waiting for the review to complete...'); - - const reviewOutcome = await reviewSession.result(); - - if (reviewOutcome.state === 'completed' || reviewOutcome.state === 'succeeded') { - // Find the agent's review message - const reviewActivities = await jules.select({ - from: 'activities', - where: { type: 'agentMessaged', 'session.id': reviewSession.id }, - order: 'desc', - limit: 1, - }); - - if (reviewActivities.length > 0) { - console.log('\n--- Review Agent Analysis ---'); - console.log(reviewActivities[0].message); - console.log('-----------------------------'); - } else { - // Fallback if the agent wrote the review to a file instead of a message - const files = reviewOutcome.generatedFiles(); - if (files.size > 0) { - console.log('\n--- Review Agent Analysis (Files) ---'); - for (const [filename, content] of files.entries()) { - console.log(`\nFile: ${filename}`); - console.log(content.content); - } - console.log('-------------------------------------'); - } else { - console.log("The review agent completed but didn't leave a message or file."); - } - } + // 5. Output Success State + if (args.json) { + console.log(JSON.stringify(result.data, null, 2)); } else { - console.error(`Review session failed or did not complete. State: ${reviewOutcome.state}`); + console.log('\n======================================='); + console.log(' REVIEW COMPLETE'); + console.log('=======================================\n'); + console.log(result.data.reviewMessage); } + }, +}); - } catch (error) { - console.error('An error occurred during the process:', error); - } -} - -// Run the example -main(); +runMain(main); diff --git a/packages/core/examples/gitpatch-review/package.json b/packages/core/examples/gitpatch-review/package.json index f2863f2..784a8ce 100644 --- a/packages/core/examples/gitpatch-review/package.json +++ b/packages/core/examples/gitpatch-review/package.json @@ -3,11 +3,14 @@ "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" }, "dependencies": { - "@google/jules-sdk": "workspace:*" + "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "zod": "^3.23.0" }, "devDependencies": { "bun-types": "^1.1.8" diff --git a/packages/core/examples/gitpatch-review/src/handler.ts b/packages/core/examples/gitpatch-review/src/handler.ts new file mode 100644 index 0000000..bdf6c6b --- /dev/null +++ b/packages/core/examples/gitpatch-review/src/handler.ts @@ -0,0 +1,194 @@ +import { jules, Session, Activity } from '@google/jules-sdk'; +import { ReviewSpec, ReviewInput, ReviewResult } from './spec.js'; + +export class ReviewHandler implements ReviewSpec { + async execute(input: ReviewInput): Promise { + try { + if (!process.env.JULES_API_KEY) { + return { + success: false, + error: { + code: 'UNAUTHORIZED', + message: 'JULES_API_KEY environment variable is not set.', + suggestion: 'Export JULES_API_KEY="your-api-key" before running the CLI.', + recoverable: true, + }, + }; + } + + this.log(`Starting code generation session for ${input.repository}...`, input.json); + + const source = { github: input.repository, baseBranch: input.baseBranch }; + + // 1. Generate bad code + const codeGenSession = await jules.session({ + prompt: input.prompt, + source, + }); + + this.log(`Code Generation Session ID: ${codeGenSession.id}`, input.json); + + await this.streamSessionActivities(codeGenSession, 'Code Gen', input.json); + const genOutcome = await codeGenSession.result(); + + if (genOutcome.state === 'failed') { + return { + success: false, + error: { + code: 'SESSION_FAILED', + message: `Code generation session failed: ${codeGenSession.id}`, + recoverable: false, + }, + }; + } + + // 2. Extract GitPatch + const snapshot = codeGenSession.snapshot(); + let changeSet; + + // Type guarding the `snapshot.changeSet` function because the underlying SDK + // abstraction may change or omit it. + if (typeof snapshot.changeSet === 'function') { + changeSet = snapshot.changeSet(); + } + + let gitPatchStr = ''; + + if (changeSet && changeSet.gitPatch && changeSet.gitPatch.unidiffPatch) { + // Prefer the raw unidiff patch from the GitPatch object if available + gitPatchStr = changeSet.gitPatch.unidiffPatch; + } else if (changeSet && typeof changeSet.parsed === 'function') { + // Fallback to rebuilding it from parsed diff + const parsed = changeSet.parsed(); + for(const file of parsed.files) { + gitPatchStr += `--- a/${file.path}\n+++ b/${file.path}\n`; + for(const chunk of file.chunks) { + gitPatchStr += `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; + for(const change of chunk.changes) { + if(change.type === 'add') gitPatchStr += `+${change.content}\n`; + if(change.type === 'del') gitPatchStr += `-${change.content}\n`; + if(change.type === 'normal') gitPatchStr += ` ${change.content}\n`; + } + } + } + } else { + // Fallback to checking generated files if changeset isn't structured nicely + const files = genOutcome.generatedFiles(); + for (const [filename, content] of files.entries()) { + gitPatchStr += `File: ${filename}\n${content.content}\n`; + } + } + + if (!gitPatchStr || gitPatchStr.trim() === '') { + return { + success: false, + error: { + code: 'NO_CHANGES_GENERATED', + message: 'The agent did not generate any code changes.', + recoverable: false, + }, + }; + } + + this.log('\n--- Extracted Patch Content ---', input.json); + this.log(gitPatchStr, input.json); + this.log('-------------------------------\n', input.json); + + this.log('Starting review session...', input.json); + + // 3. Review the code + const reviewSession = await jules.session({ + prompt: `Review the following code change patch and determine if it adheres to clean coding standards. +Provide a short summary of the issues found and how they should be fixed. + +## Git Patch +\`\`\`diff +${gitPatchStr} +\`\`\` +`, + source, + }); + + this.log(`Review Session ID: ${reviewSession.id}`, input.json); + + let reviewMessage = ''; + + // We will listen for the final agent message from the stream + for await (const activity of reviewSession.stream()) { + this.logActivity(activity, 'Review', input.json); + if (activity.type === 'agentMessaged') { + reviewMessage = activity.message; + } + } + + const reviewOutcome = await reviewSession.result(); + + if (reviewOutcome.state === 'failed') { + return { + success: false, + error: { + code: 'SESSION_FAILED', + message: `Review session failed: ${reviewSession.id}`, + recoverable: false, + }, + }; + } + + if (!reviewMessage) { + // Check files if no message + const files = reviewOutcome.generatedFiles(); + if (files.size > 0) { + for (const [filename, content] of files.entries()) { + reviewMessage += `\nFile: ${filename}\n${content.content}\n`; + } + } else { + reviewMessage = "The review completed but the agent provided no feedback."; + } + } + + return { + success: true, + data: { + codeGenSessionId: codeGenSession.id, + reviewSessionId: reviewSession.id, + gitPatchStr, + reviewMessage, + }, + }; + + } catch (error) { + return { + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : String(error), + recoverable: false, + }, + }; + } + } + + private async streamSessionActivities(session: Session, prefix: string, isJson: boolean) { + for await (const activity of session.stream()) { + this.logActivity(activity, prefix, isJson); + } + } + + private logActivity(activity: Activity, prefix: string, isJson: boolean) { + if (activity.type === 'progressUpdated') { + this.log(`[${prefix}] ${activity.title}`, isJson); + } else if (activity.type === 'agentMessaged') { + this.log(`[${prefix} Agent]: ${activity.message}`, isJson); + } else if (activity.type === 'planGenerated') { + this.log(`[${prefix}] Generated plan with ${activity.plan.steps.length} steps.`, isJson); + } + } + + private log(message: string, isJson: boolean) { + if (isJson) { + console.error(message); + } else { + console.log(message); + } + } +} diff --git a/packages/core/examples/gitpatch-review/src/spec.ts b/packages/core/examples/gitpatch-review/src/spec.ts new file mode 100644 index 0000000..0878041 --- /dev/null +++ b/packages/core/examples/gitpatch-review/src/spec.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +// 1. INPUT (The Command) - "Parse, don't validate" +export const ReviewInputSchema = z.object({ + repository: z.string().min(1, 'Repository must be provided (e.g., owner/repo)'), + baseBranch: z.string().min(1, 'Base branch must be provided (e.g., main)'), + prompt: z.string().min(1, 'A prompt to generate code must be provided'), + json: z.boolean().default(false), +}); + +export type ReviewInput = z.infer; + +// 2. ERROR CODES (Exhaustive) +export const ReviewErrorCode = z.enum([ + 'SESSION_FAILED', + 'NO_CHANGES_GENERATED', + 'UNKNOWN_ERROR', + 'UNAUTHORIZED', +]); + +// 3. RESULT (The Monad) +export const ReviewSuccess = z.object({ + success: z.literal(true), + data: z.object({ + reviewMessage: z.string(), + codeGenSessionId: z.string(), + reviewSessionId: z.string(), + gitPatchStr: z.string(), + }), +}); + +export const ReviewFailure = z.object({ + success: z.literal(false), + error: z.object({ + code: ReviewErrorCode, + message: z.string(), + suggestion: z.string().optional(), + recoverable: z.boolean(), + }), +}); + +export type ReviewResult = z.infer | z.infer; + +// 4. INTERFACE (The Capability) +export interface ReviewSpec { + execute(input: ReviewInput): Promise; +} From 2266af62c18806ff0fd3cce94e2a93eca725e074 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 02:17:26 +0000 Subject: [PATCH 3/3] feat(sdk): Add GitPatch Review CLI example using Typed Service Contract Adds a new practical example to the Jules TypeScript SDK demonstrating how to use the SDK to generate code and review the resulting GitPatch against standard coding practices. The example is built as an Agent-ready CLI using the `citty` framework, follows the Typed Service Contract pattern to separate schemas from business logic, streams activities back to the user, and uses the `session.snapshot()` to safely extract the GitPatch. Includes an automated E2E test script (`e2e-test.ts`) to verify authentic LLM execution flows. Updates the main packages/core/README.md to include a link to the new example. Included a new package in packages/core/examples/gitpatch-review. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 31 ++++++ .../core/examples/gitpatch-review/e2e-test.ts | 104 ++++++++++++++++++ .../examples/gitpatch-review/package.json | 6 +- .../examples/gitpatch-review/src/handler.ts | 31 ++++-- 4 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 packages/core/examples/gitpatch-review/e2e-test.ts diff --git a/bun.lock b/bun.lock index e99d7a7..213b7a6 100644 --- a/bun.lock +++ b/bun.lock @@ -75,6 +75,7 @@ }, "devDependencies": { "bun-types": "^1.1.8", + "execa": "^9.6.1", }, }, "packages/core/examples/webhook": { @@ -451,6 +452,8 @@ "@rushstack/ts-command-line": ["@rushstack/ts-command-line@5.1.5", "", { "dependencies": { "@rushstack/terminal": "0.19.5", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-YmrFTFUdHXblYSa+Xc9OO9FsL/XFcckZy0ycQ6q7VSBsVs5P0uD9vcges5Q9vctGlVdu27w+Ct6IuJ458V0cTQ=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="], @@ -465,6 +468,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], "@types/aws-lambda": ["@types/aws-lambda@8.10.160", "", {}, "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA=="], @@ -669,6 +674,8 @@ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -687,6 +694,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="], @@ -707,6 +716,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], @@ -743,6 +754,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], @@ -759,10 +772,16 @@ "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], @@ -851,6 +870,8 @@ "niftty": ["niftty@0.1.3", "", { "dependencies": { "chalk": "^5.6.2", "shiki": "^3.12.2", "string-length": "^6.0.0", "tinycolor2": "^1.6.0" } }, "sha512-wy8Kysxzh/R3hBq0BDlBbnzxDU/b/3PUtWfWVm1KwOestaVF3423U4iHD7TthPMF/RTHPXGenxh6YNERaD8M2g=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -871,6 +892,8 @@ "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -901,6 +924,8 @@ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -993,6 +1018,8 @@ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], @@ -1139,6 +1166,8 @@ "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1237,6 +1266,8 @@ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "octokit/@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="], "octokit/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], diff --git a/packages/core/examples/gitpatch-review/e2e-test.ts b/packages/core/examples/gitpatch-review/e2e-test.ts new file mode 100644 index 0000000..55647f7 --- /dev/null +++ b/packages/core/examples/gitpatch-review/e2e-test.ts @@ -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(); diff --git a/packages/core/examples/gitpatch-review/package.json b/packages/core/examples/gitpatch-review/package.json index 784a8ce..b76a135 100644 --- a/packages/core/examples/gitpatch-review/package.json +++ b/packages/core/examples/gitpatch-review/package.json @@ -5,7 +5,8 @@ "main": "index.ts", "type": "module", "scripts": { - "start": "bun run index.ts" + "start": "bun run index.ts", + "test:e2e": "bun run e2e-test.ts" }, "dependencies": { "@google/jules-sdk": "workspace:*", @@ -13,6 +14,7 @@ "zod": "^3.23.0" }, "devDependencies": { - "bun-types": "^1.1.8" + "bun-types": "^1.1.8", + "execa": "^9.6.1" } } diff --git a/packages/core/examples/gitpatch-review/src/handler.ts b/packages/core/examples/gitpatch-review/src/handler.ts index bdf6c6b..afaf84e 100644 --- a/packages/core/examples/gitpatch-review/src/handler.ts +++ b/packages/core/examples/gitpatch-review/src/handler.ts @@ -28,7 +28,17 @@ export class ReviewHandler implements ReviewSpec { this.log(`Code Generation Session ID: ${codeGenSession.id}`, input.json); - await this.streamSessionActivities(codeGenSession, 'Code Gen', input.json); + // If the session isn't immediately finished, stream it until it is + const genInfo = await codeGenSession.info(); + if (genInfo.state !== 'completed' && genInfo.state !== 'failed') { + try { + await this.streamSessionActivities(codeGenSession, 'Code Gen', input.json); + } catch(e) { + // Occasionally activities return 404 momentarily immediately after session creation + // in some environments. Ignore and fall through to wait on result(). + } + } + const genOutcome = await codeGenSession.result(); if (genOutcome.state === 'failed') { @@ -113,12 +123,19 @@ ${gitPatchStr} let reviewMessage = ''; - // We will listen for the final agent message from the stream - for await (const activity of reviewSession.stream()) { - this.logActivity(activity, 'Review', input.json); - if (activity.type === 'agentMessaged') { - reviewMessage = activity.message; - } + // Stream to get live updates and block until finished + const revInfo = await reviewSession.info(); + if (revInfo.state !== 'completed' && revInfo.state !== 'failed') { + try { + for await (const activity of reviewSession.stream()) { + this.logActivity(activity, 'Review', input.json); + if (activity.type === 'agentMessaged') { + reviewMessage = activity.message; + } + } + } catch(e) { + // Ignore stream fetch errors + } } const reviewOutcome = await reviewSession.result();