
Automate Github Issues
- 701 installs
- 84 repo stars
- Updated June 4, 2026
- google-labs-code/jules-skills
automate-github-issues is a Claude Code skill that schedules Jules fleet sessions to fetch open GitHub issues and dispatch parallel coding agents for developers who want issue backlog work automated without manual triage
About
automate-github-issues is a Claude Code skill from google-labs-code/jules-skills that orchestrates Jules fleet sessions against a GitHub repository's open issues. Configuration requires JULES_API_KEY for dispatching sessions and GITHUB_TOKEN with repo access for issue fetching and optional PR merge, with an optional FLEET_BASE_BRANCH override defaulting to the current branch. Developers reach for this skill when open issues should fan out to parallel Jules agents instead of sitting in a manual queue. The workflow copies environment variables from the bundled .env template, then schedules fleet runs that pull issues and assign coding agents automatically. It fits teams experimenting with agent fleets on maintenance backlogs, dependency bumps, or labeled good-first-issue queues where Jules can propose patches.
- Daily cron or manual workflow_dispatch Fleet Dispatch GitHub Action
- Requires JULES_API_KEY and GITHUB_TOKEN for sessions, issue fetch, and PR merge
- Optional FLEET_BASE_BRANCH override for fleet session base branch
- Concurrency group fleet-dispatch prevents overlapping fleet runs
- Bootstrap prompt drives in-repo fleet scripts to analyze and dispatch on open issues
Automate Github Issues by the numbers
- 701 all-time installs (skills.sh)
- Ranked #356 of 2,742 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/google-labs-code/jules-skills --skill automate-github-issuesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 701 |
|---|---|
| repo stars | ★ 84 |
| Security audit | 1 / 3 scanners passed |
| Last updated | June 4, 2026 |
| Repository | google-labs-code/jules-skills ↗ |
How do you automate GitHub issues with Jules?
Schedule Jules fleet sessions that fetch open GitHub issues and dispatch parallel coding agents to work them without manual triage.
Who is it for?
Developers operating Google Jules with repo-scoped tokens who want parallel agents to chew through labeled GitHub issue backlogs automatically.
Skip if: Skip automate-github-issues when issues need human design review first, Jules API access is unavailable, or GitHub tokens lack repo write permissions.
When should I use this skill?
The developer wants Jules fleet sessions to fetch GitHub issues and run parallel coding agents without manual issue-by-issue assignment.
What you get
Scheduled Jules fleet sessions, dispatched parallel agent runs per open issue, and optional PR merges from the configured base branch.
- Jules fleet session schedule
- Parallel agent dispatches per issue
By the numbers
- Requires 2 mandatory environment variables: JULES_API_KEY and GITHUB_TOKEN
- Supports 1 optional FLEET_BASE_BRANCH override for fleet session targeting
Files
Automate GitHub Issues with Jules
You are setting up a repository to automatically analyze open GitHub issues, plan implementation tasks, and dispatch parallel Jules coding agents to fix them.
What You're Setting Up
A 5-phase automated pipeline that runs via GitHub Actions (or locally):
1. Analyze — Fetch open issues and format as structured markdown 2. Plan — A Jules session performs deep code-level triage and produces self-contained task prompts 3. Validate — Verify no two tasks modify the same file (prevents merge conflicts) 4. Dispatch — Spawn parallel Jules sessions, one per task 5. Merge — Sequential PR merge with CI validation
Setup Steps
Step 1: Copy fleet scripts to the repository
Copy the entire scripts/ directory from this skill into the target repository at scripts/fleet/:
Target structure:
scripts/fleet/
├── fleet-analyze.ts
├── fleet-plan.ts
├── fleet-dispatch.ts
├── fleet-merge.ts
├── types.ts
├── prompts/
│ ├── analyze-issues.ts
│ └── bootstrap.ts
└── github/
├── git.ts
├── issues.ts
├── markdown.ts
└── cache-plugin.tsImportant: Preserve the directory structure exactly. The scripts use relative imports between files.
Step 2: Copy workflow templates
Copy the workflow files from assets/ to the repository's .github/workflows/ directory:
assets/fleet-dispatch.yml→.github/workflows/fleet-dispatch.ymlassets/fleet-merge.yml→.github/workflows/fleet-merge.yml
Step 3: Create a package.json for the fleet scripts
Create scripts/fleet/package.json with the required dependencies:
{
"name": "fleet-scripts",
"private": true,
"type": "module",
"dependencies": {
"@google/jules-sdk": "^0.1.0",
"octokit": "^4.1.0",
"find-up": "^7.0.0"
},
"devDependencies": {
"@types/bun": "^1.2.0"
}
}Step 4: Create environment template
Copy assets/.env.example to the repository root.
Step 5: Install dependencies
cd scripts/fleet && bun installStep 6: Print next steps for the user
Tell the user they need to: 1. Add JULES_API_KEY as a GitHub repository secret (Settings → Secrets → Actions) 2. GITHUB_TOKEN is provided automatically by GitHub Actions 3. Customize the cron schedule in .github/workflows/fleet-dispatch.yml (default: daily 6am UTC) 4. Commit all generated files
Manual Usage
After setup, the user can run the pipeline locally:
cd scripts/fleet
# Fetch open issues
bun fleet-analyze.ts
# Plan tasks (creates a Jules planning session)
JULES_API_KEY=<key> bun fleet-plan.ts
# Dispatch parallel agents
JULES_API_KEY=<key> bun fleet-dispatch.ts
# Merge PRs sequentially
GITHUB_TOKEN=<token> bun fleet-merge.tsCustomization
Prompt Tuning
The analysis prompt in scripts/fleet/prompts/analyze-issues.ts controls how deeply issues are investigated. Users can adjust:
- Root cause analysis depth
- Solution implementation detail level
- Merge conflict avoidance rules
- File ownership constraints
Issue Filtering
Edit scripts/fleet/github/issues.ts to filter issues by label, milestone, or state.
Resource References
- Architecture Overview — Detailed explanation of the 5-phase pipeline
Troubleshooting
- "Unable to parse git remote URL": Ensure the repo has a valid GitHub remote (
git remote get-url origin) - Ownership conflict errors: Two tasks claim the same file. Adjust the task JSON or merge them manually.
- CI timeout during merge: Increase
maxWaitMsinfleet-merge.ts(default: 10 minutes) - Bun not found: Install Bun:
curl -fsSL https://bun.sh/install | bash
# Environment variables for automate-github-issues skill
# Copy this to .env and fill in your values
# Your Jules API key (required for dispatching sessions)
JULES_API_KEY=
# GitHub personal access token with repo access (required for issue fetching and PR merge)
GITHUB_TOKEN=
# Optional: override the base branch for fleet sessions (defaults to current branch)
# FLEET_BASE_BRANCH=main
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# .github/workflows/fleet-dispatch.yml
#
# Creates a Jules session to analyze open issues and dispatch parallel agents.
# The session receives a bootstrap prompt and runs the fleet scripts from the repo.
name: Fleet Dispatch
on:
schedule:
# Runs daily at 6am UTC (customize as needed)
- cron: "0 6 * * *"
workflow_dispatch:
inputs:
base_branch:
description: "Base branch for Jules sessions"
type: string
default: "main"
concurrency:
group: fleet-dispatch
cancel-in-progress: false
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.3.1"
- name: Install fleet dependencies
working-directory: scripts/fleet
run: bun install
- name: Create planning session
working-directory: scripts/fleet
env:
JULES_API_KEY: ${{ secrets.JULES_API_KEY }}
FLEET_BASE_BRANCH: ${{ inputs.base_branch || 'main' }}
run: bun fleet-plan.ts
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# .github/workflows/fleet-merge.yml
#
# Sequentially merges Jules-authored PRs: update branch → wait for CI → squash merge.
# On merge conflict, re-dispatches the task as a new Jules session against current base.
name: Fleet Sequential Merge
on:
# Allow manual trigger (recommended: review PRs first, then trigger)
workflow_dispatch:
inputs:
base_branch:
description: "Base branch for merge"
type: string
default: "main"
concurrency:
group: fleet-merge
cancel-in-progress: false
jobs:
sequential-merge:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JULES_API_KEY: ${{ secrets.JULES_API_KEY }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install fleet dependencies
run: cd scripts/fleet && bun install
- name: Find and merge fleet PRs sequentially
run: |
set -euo pipefail
BASE_BRANCH="${{ inputs.base_branch || 'main' }}"
MAX_CI_WAIT=600 # 10 minutes per PR
MAX_RETRIES=2 # Max re-dispatch attempts per PR
PR_POLL_TIMEOUT=900 # 15 minutes to wait for re-dispatched PR
echo "🔍 Finding open Jules-authored PRs targeting ${BASE_BRANCH}..."
# Get all open PRs authored by Jules, sorted by creation date (oldest first)
PRS=$(gh pr list \
--state open \
--base "$BASE_BRANCH" \
--json number,headRefName,author \
--jq '[.[] | select(.author.login == "google-labs-jules" or (.author.login | endswith("[bot]")))] | sort_by(.number) | .[].number')
if [ -z "$PRS" ]; then
echo "ℹ️ No Jules-authored PRs found. Nothing to merge."
exit 0
fi
PR_COUNT=$(echo "$PRS" | wc -l | tr -d ' ')
echo "Found ${PR_COUNT} Jules PR(s) to merge."
for PR_NUM in $PRS; do
echo ""
echo "📦 Processing PR #${PR_NUM}..."
RETRY_COUNT=0
while true; do
# Update branch from base
echo " 🔄 Updating branch from ${BASE_BRANCH}..."
UPDATE_OUTPUT=$(gh pr update-branch "$PR_NUM" --rebase 2>&1) || true
# Check for merge conflict
if echo "$UPDATE_OUTPUT" | grep -qi "conflict\|cannot be rebased"; then
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo " ❌ Conflict persists after ${MAX_RETRIES} retries. Human intervention required."
echo " PR: $(gh pr view "$PR_NUM" --json url --jq '.url')"
exit 1
fi
echo " ⚠️ Merge conflict detected. Re-dispatching task..."
# Get the PR's task prompt from the fleet data
TASK_PROMPT=$(gh pr view "$PR_NUM" --json body --jq '.body')
# Close the conflicting PR
echo " 🔒 Closing conflicting PR #${PR_NUM}..."
gh pr close "$PR_NUM" --comment "⚠️ Closed by fleet-merge: merge conflict detected. Task re-dispatched as a new session."
# Re-dispatch via Jules SDK
echo " 🚀 Re-dispatching against current ${BASE_BRANCH}..."
REPO_FULL="${{ github.repository }}"
NEW_SESSION_ID=$(bun -e "
import { jules } from '@google/jules-sdk';
const session = await jules.createSession({
prompt: process.env.TASK_PROMPT,
source: { github: '${REPO_FULL}', baseBranch: '${BASE_BRANCH}' },
});
console.log(session.id);
" 2>/dev/null)
echo " 📝 New session: ${NEW_SESSION_ID}"
# Poll for new PR
echo " ⏳ Waiting for new PR from session ${NEW_SESSION_ID}..."
POLL_ELAPSED=0
NEW_PR_NUM=""
while [ $POLL_ELAPSED -lt $PR_POLL_TIMEOUT ]; do
sleep 30
POLL_ELAPSED=$((POLL_ELAPSED + 30))
NEW_PR_NUM=$(gh pr list --state open --base "$BASE_BRANCH" --json number,headRefName,body \
--jq "[.[] | select(.headRefName | contains(\"${NEW_SESSION_ID}\")) // select(.body | contains(\"${NEW_SESSION_ID}\"))] | .[0].number // empty")
if [ -n "$NEW_PR_NUM" ]; then
echo " ✅ New PR #${NEW_PR_NUM} found."
break
fi
echo " ⏳ No PR yet... (${POLL_ELAPSED}s/${PR_POLL_TIMEOUT}s)"
done
if [ -z "$NEW_PR_NUM" ]; then
echo " ❌ Timed out waiting for re-dispatched PR. Human intervention required."
exit 1
fi
PR_NUM="$NEW_PR_NUM"
RETRY_COUNT=$((RETRY_COUNT + 1))
continue
fi
# Wait for branch update to propagate
sleep 5
# Wait for CI checks to pass
echo " 🧪 Waiting for CI checks..."
ELAPSED=0
CI_PASSED=false
while [ $ELAPSED -lt $MAX_CI_WAIT ]; do
STATUS=$(gh pr checks "$PR_NUM" --json name,state --jq '[.[].state] | if length == 0 then "none" elif all(. == "SUCCESS" or . == "SKIPPED") then "pass" elif any(. == "FAILURE") then "fail" else "pending" end')
case "$STATUS" in
"pass")
CI_PASSED=true
break
;;
"none")
echo " ℹ️ No CI checks configured. Proceeding."
CI_PASSED=true
break
;;
"fail")
echo " ❌ CI failed for PR #${PR_NUM}. Skipping."
break
;;
*)
echo " ⏳ CI pending... (${ELAPSED}s/${MAX_CI_WAIT}s)"
sleep 30
ELAPSED=$((ELAPSED + 30))
;;
esac
done
if [ "$CI_PASSED" = false ]; then
echo " ⏭️ Skipping PR #${PR_NUM} (CI did not pass)"
else
# Squash merge
echo " ✅ CI passed. Merging PR #${PR_NUM}..."
if gh pr merge "$PR_NUM" --squash; then
echo " 🎉 PR #${PR_NUM} merged successfully."
else
echo " ❌ Failed to merge PR #${PR_NUM}. Stopping sequential merge."
exit 1
fi
fi
# Exit the retry loop for this PR
break
done
# Brief pause for merge to propagate before next PR
sleep 5
done
echo ""
echo "✅ Sequential merge complete."
{
"name": "automate-github-issues",
"private": true,
"type": "module",
"scripts": {
"setup": "bash scripts/setup.sh",
"analyze": "bun run fleet-analyze.ts",
"plan": "bun run fleet-plan.ts",
"dispatch": "bun run fleet-dispatch.ts",
"merge": "bun run fleet-merge.ts"
},
"dependencies": {
"@google/jules-sdk": "^0.1.0",
"octokit": "^4.1.0",
"find-up": "^7.0.0"
},
"devDependencies": {
"@types/bun": "^1.2.0"
},
"engines": {
"node": ">=18.0.0"
}
}Automate GitHub Issues
An Agent Skill that sets up your repository to automatically triage and fix GitHub issues using parallel Jules coding agents.
What It Does
When activated, this skill bootstraps your repository with a 5-phase automated pipeline:
Example Prompt
Set up this GitHub repository to automate issue fixes with Jules.What Gets Created
The skill copies the following into your repository:
scripts/fleet/ # Pipeline scripts (committed to your repo)
├── fleet-analyze.ts
├── fleet-plan.ts
├── fleet-dispatch.ts
├── fleet-merge.ts
├── package.json
├── prompts/
│ ├── analyze-issues.ts # Issue analysis prompt template
│ └── bootstrap.ts # Bootstrap prompt for scheduled sessions
└── github/
├── git.ts # Git repo utilities
├── issues.ts # GitHub issue fetching
├── markdown.ts # Issue → markdown formatting
└── cache-plugin.ts # ETag-based API caching
.github/workflows/
├── fleet-dispatch.yml # Scheduled dispatch (daily cron)
└── fleet-merge.yml # Auto-merge Jules PRsPrerequisites
- Bun runtime
- A Jules API key
- GitHub token with repo access
Pipeline Overview
flowchart LR
A["📊 Analyze"] --> B["🧠 Plan"]
B --> C["✅ Validate"]
C --> D["🚀 Dispatch"]
D --> E["🔀 Merge"]| Phase | Script | What it does |
|---|---|---|
| Analyze | fleet-analyze.ts | Fetches open issues → structured markdown |
| Plan | fleet-plan.ts | Jules diagnoses root causes, builds File Ownership Matrix |
| Validate | fleet-dispatch.ts | Checks no two tasks claim the same file |
| Dispatch | fleet-dispatch.ts | Spawns parallel Jules sessions via jules.all() |
| Merge | fleet-merge.ts | Sequential merge: update branch → CI → squash |
Detailed Flow
flowchart TD
subgraph analyze ["Phase 1: Analyze"]
A1["Fetch open GitHub issues"] --> A2["Format as structured markdown"]
end
subgraph plan ["Phase 2: Plan"]
A2 --> B1["Create Jules planning session"]
B1 --> B2["Investigate: trace issues to source code"]
B2 --> B3["Architect: design solutions with diffs"]
B3 --> B4["Build File Ownership Matrix"]
B4 --> B5{"Any file in 2+ tasks?"}
B5 -- Yes --> B6["Merge overlapping tasks"]
B6 --> B4
B5 -- No --> B7["Write task plan to .fleet/"]
end
subgraph validate ["Phase 3: Validate"]
B7 --> C1["Read issue_tasks.json"]
C1 --> C2{"Ownership conflict?"}
C2 -- Yes --> C3["❌ Abort before dispatch"]
C2 -- No --> C4["✅ Safe to parallelize"]
end
subgraph dispatch ["Phase 4: Dispatch"]
C4 --> D1["jules.all — spawn parallel sessions"]
D1 --> D2["Each session targets same base branch"]
D2 --> D3["Sessions produce PRs"]
end
subgraph merge ["Phase 5: Merge"]
D3 --> E1["Process PRs sequentially by risk"]
E1 --> E2["Update branch from base"]
E2 --> E3{"Merge conflict?"}
E3 -- No --> E4["Wait for CI"]
E4 --> E5{"CI passed?"}
E5 -- Yes --> E6["Squash merge"]
E5 -- No --> E7["❌ Abort"]
E6 --> E8{"More PRs?"}
E8 -- Yes --> E1
E8 -- No --> E9["✅ All merged"]
E3 -- Yes --> E10{"Retries left?"}
E10 -- No --> E11["❌ Escalate to human"]
E10 -- Yes --> E12["Close old PR"]
E12 --> E13["Re-dispatch: new Jules session\nagainst current base"]
E13 --> E14["Wait for new PR"]
E14 --> E2
end
style analyze fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style plan fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style validate fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style dispatch fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style merge fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0Manual Usage
After setup, run the pipeline locally:
cd scripts/fleet
# Fetch open issues
bun fleet-analyze.ts
# Plan tasks (creates a Jules planning session)
JULES_API_KEY=<key> bun fleet-plan.ts
# Dispatch parallel agents
JULES_API_KEY=<key> bun fleet-dispatch.ts
# Merge PRs sequentially
GITHUB_TOKEN=<token> bun fleet-merge.tsSetup (after skill activation)
1. Set Secrets
Add JULES_API_KEY as a GitHub repository secret (Settings → Secrets → Actions). GITHUB_TOKEN is provided automatically by GitHub Actions.
2. Customize
- Adjust the cron schedule in
.github/workflows/fleet-dispatch.yml(default: daily 6am UTC) - Tune the analysis prompt in
scripts/fleet/prompts/analyze-issues.ts
3. Commit
Commit all generated files and push.
This is not an officially supported Google product.
Architecture: Automate GitHub Issues
This document describes the 5-phase pipeline that gets installed into your repository.
Pipeline Overview
flowchart LR
A[Analyze] --> B[Plan]
B --> C[Validate]
C --> D[Dispatch]
D --> E[Merge]Phase 1: Analyze
Script: fleet-analyze.ts
Fetches all open GitHub issues using the Octokit API with ETag caching. Formats them into a structured markdown document that includes:
- Issue number, title, author, labels
- State, timestamps, reactions
- Full description body
Output: Markdown string passed to the planner.
Phase 2: Plan
Script: fleet-plan.ts Prompt: prompts/analyze-issues.ts
Creates a Jules session that performs deep code-level triage:
1. Investigate — Trace each issue to its root cause in the codebase, referencing specific files, functions, and line ranges. 2. Architect — Design concrete solutions with TypeScript implementation code, integration diffs, and test scenarios. 3. Plan — Group root causes into tasks, produce a File Ownership Matrix ensuring no two tasks touch the same file. 4. Dispatch — Write the task plan to .fleet/{date}/issue_tasks.json and .fleet/{date}/issue_tasks.md.
Critical constraint: Merge conflict avoidance. Tasks are dispatched as parallel agents, so file ownership must be exclusive.
Phase 3: Validate
Script: fleet-dispatch.ts (built in)
Before dispatching, the orchestrator validates ownership:
- Builds a map of every file claimed by each task (source, new, and test files)
- Throws if any file appears in more than one task
- This prevents merge conflicts when parallel PRs land
Phase 4: Dispatch
Script: fleet-dispatch.ts
Spawns parallel Jules sessions using jules.all():
- Each task gets its own session with a self-contained, code-rich prompt
- Sessions target the same base branch
- Session IDs are written to
.fleet/{date}/sessions.jsonfor the merge phase
Phase 5: Merge
Script: fleet-merge.ts (local) / fleet-merge.yml (GitHub Action)
Processes fleet PRs sequentially in risk order (lowest first):
1. Find open PRs matching session IDs (local) or Jules author (GitHub Action) 2. For each PR: update branch from base 3. Wait for CI to pass (polling every 30s, timeout after 10min) 4. Squash-merge 5. Move to the next PR
If a merge conflict is detected during branch update, the process stops and reports the PR URL for human intervention.
File Structure (after setup)
scripts/fleet/
├── fleet-analyze.ts # Fetches open issues as markdown
├── fleet-plan.ts # Creates the planning session
├── fleet-dispatch.ts # Validates ownership + dispatches Jules sessions
├── fleet-merge.ts # Sequential PR merge with CI wait (local use)
├── types.ts # Shared TypeScript types
├── package.json # Fleet script dependencies
├── prompts/
│ ├── analyze-issues.ts # The 4-phase analysis prompt
│ └── bootstrap.ts # Wraps prompt for scheduled sessions
└── github/
├── git.ts # Git remote parsing (owner/repo/branch)
├── issues.ts # GitHub issue fetching with cache
├── markdown.ts # Issue → markdown formatting
└── cache-plugin.ts # Octokit ETag cache pluginScheduled Automation
The fleet-dispatch.yml workflow runs the planning phase on a cron schedule:
1. Installs dependencies from scripts/fleet/package.json 2. Runs fleet-plan.ts to create a Jules planning session 3. The session fetches issues, analyzes them, and dispatches N parallel agents 4. Each agent produces a PR 5. The fleet-merge.yml workflow triggers on PR open events and merges them sequentially using gh CLI
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { getIssuesAsMarkdown } from "./github/markdown.js";
async function main() {
try {
const markdown = await getIssuesAsMarkdown();
console.log(markdown);
} catch (error) {
console.error("Error fetching issues:", error);
process.exit(1);
}
}
main();
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import path from "node:path";
import { findUpSync } from "find-up";
import type { IssueAnalysis } from "./types.js";
import { jules } from "@google/jules-sdk";
import { getGitRepoInfo, getCurrentBranch } from "./github/git.js";
const date = new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit" })
.format(new Date())
.replaceAll("-", "_");
const root = path.dirname(findUpSync(".git")!);
const fleetDir = path.join(root, ".fleet", date);
const tasksPath = path.join(fleetDir, "issue_tasks.json");
const analysis = await Bun.file(tasksPath).json() as IssueAnalysis;
const { tasks } = analysis;
// Resolve repo info dynamically from git remote
const repoInfo = await getGitRepoInfo();
const baseBranch = process.env.FLEET_BASE_BRANCH ?? await getCurrentBranch();
// Pre-dispatch ownership validation
function validateOwnership(analysis: IssueAnalysis): void {
const claimed = new Map<string, string>();
for (const task of analysis.tasks) {
const allFiles = [...task.files, ...task.new_files, ...(task.test_files ?? [])];
for (const file of allFiles) {
const existing = claimed.get(file);
if (existing) {
throw new Error(
`Ownership conflict: "${file}" claimed by both "${existing}" and "${task.id}". These tasks must be merged.`
);
}
claimed.set(file, task.id);
}
}
}
validateOwnership(analysis);
console.log(`✅ Ownership validated: ${analysis.tasks.length} tasks, no conflicts.`);
const sessions = await jules.all(tasks, task => ({
prompt: task.prompt,
source: {
github: repoInfo.fullName,
baseBranch,
}
}))
const sessionResults: Array<{ taskId: string; sessionId: string }> = [];
for await (const session of sessions) {
const taskId = tasks[sessionResults.length]?.id ?? "unknown";
sessionResults.push({ taskId, sessionId: session.id });
console.log(`Task ${taskId} → Session ${session.id}`);
}
// Write session mapping for fleet-merge.ts
const sessionsPath = path.join(fleetDir, "sessions.json");
await Bun.write(sessionsPath, JSON.stringify(sessionResults, null, 2));
console.log(`📝 Session mapping written to ${sessionsPath}`);
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import path from "node:path";
import { findUpSync } from "find-up";
import type { IssueAnalysis, Task } from "./types.js";
import { getGitRepoInfo, getCurrentBranch } from "./github/git.js";
import { jules } from "@google/jules-sdk";
const repoInfo = await getGitRepoInfo();
const OWNER = repoInfo.owner;
const REPO = repoInfo.repo;
const BASE_BRANCH = process.env.FLEET_BASE_BRANCH ?? "main";
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
// Re-dispatch configuration
const MAX_RETRIES = Number(process.env.FLEET_MAX_RETRIES ?? 2);
const PR_POLL_INTERVAL_MS = 30_000;
const PR_POLL_TIMEOUT_MS = 15 * 60 * 1000;
if (!GITHUB_TOKEN) {
console.error("❌ GITHUB_TOKEN environment variable is required.");
process.exit(1);
}
const headers = {
Authorization: `Bearer ${GITHUB_TOKEN}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
} as const;
const API = `https://api.github.com/repos/${OWNER}/${REPO}`;
const date = new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit" })
.format(new Date())
.replaceAll("-", "_");
const root = path.dirname(findUpSync(".git")!);
const fleetDir = path.join(root, ".fleet", date);
// Load task ordering (already sorted by risk in the analysis phase)
const analysis = await Bun.file(path.join(fleetDir, "issue_tasks.json")).json() as IssueAnalysis;
// Load session mapping written by fleet-dispatch.ts
const sessions = await Bun.file(path.join(fleetDir, "sessions.json")).json() as Array<{
taskId: string;
sessionId: string;
}>;
interface GitHubPR {
number: number;
head: { ref: string };
body: string | null;
}
// Find open PRs created by fleet sessions
async function findFleetPRs() {
const res = await fetch(`${API}/pulls?state=open&per_page=100`, { headers });
const pulls = (await res.json()) as GitHubPR[];
const prMap = new Map<string, GitHubPR>();
for (const session of sessions) {
const matchingPR = pulls.find((pr: GitHubPR) =>
pr.head.ref.includes(session.sessionId) ||
pr.body?.includes(session.sessionId)
);
if (matchingPR) {
prMap.set(session.taskId, matchingPR);
}
}
return prMap;
}
interface CheckRun {
status: string;
conclusion: string | null;
}
async function waitForCI(prNumber: number, maxWaitMs = 10 * 60 * 1000): Promise<boolean> {
const start = Date.now();
// First, get the head SHA for this PR
const prRes = await fetch(`${API}/pulls/${prNumber}`, { headers });
const prData = (await prRes.json()) as { head: { sha: string } };
const headSha = prData.head.sha;
while (Date.now() - start < maxWaitMs) {
const res = await fetch(`${API}/commits/${headSha}/check-runs`, { headers });
const data = (await res.json()) as { check_runs: CheckRun[] };
// No CI configured — skip validation
if (data.check_runs.length === 0) {
console.log(` ℹ️ No check runs found for PR #${prNumber}. Proceeding without CI.`);
return true;
}
const allComplete = data.check_runs.every((run: CheckRun) => run.status === "completed");
const allPassed = data.check_runs.every((run: CheckRun) =>
run.conclusion === "success" || run.conclusion === "skipped"
);
if (allComplete && allPassed) return true;
if (allComplete && !allPassed) return false;
console.log(` ⏳ CI still running for PR #${prNumber}... waiting 30s`);
await new Promise(r => setTimeout(r, 30_000));
}
console.log(` ⏰ CI timeout for PR #${prNumber}`);
return false;
}
// Re-dispatch a task as a new Jules session against current main
async function redispatchTask(
task: Task,
oldPr: GitHubPR,
): Promise<GitHubPR> {
// Close the conflicting PR
console.log(` 🔒 Closing conflicting PR #${oldPr.number}...`);
await fetch(`${API}/pulls/${oldPr.number}`, {
method: "PATCH",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
state: "closed",
body: `${oldPr.body ?? ""}\n\n---\n⚠️ Closed by fleet-merge: merge conflict detected. Task re-dispatched as a new session.`,
}),
});
// Create a new Jules session with the same prompt
console.log(` 🚀 Re-dispatching task "${task.id}" against current ${BASE_BRANCH}...`);
const session = await jules.createSession({
prompt: task.prompt,
source: {
github: `${OWNER}/${REPO}`,
baseBranch: BASE_BRANCH,
},
});
console.log(` 📝 New session: ${session.id}`);
// Update sessions.json with new session ID
const sessionEntry = sessions.find(s => s.taskId === task.id);
if (sessionEntry) {
sessionEntry.sessionId = session.id;
const sessionsPath = path.join(fleetDir, "sessions.json");
await Bun.write(sessionsPath, JSON.stringify(sessions, null, 2));
}
// Poll for the new PR
console.log(` ⏳ Waiting for new PR from session ${session.id}...`);
const start = Date.now();
while (Date.now() - start < PR_POLL_TIMEOUT_MS) {
await new Promise(r => setTimeout(r, PR_POLL_INTERVAL_MS));
const res = await fetch(`${API}/pulls?state=open&per_page=100`, { headers });
const pulls = (await res.json()) as GitHubPR[];
const newPr = pulls.find(
(pr: GitHubPR) =>
pr.head.ref.includes(session.id) ||
pr.body?.includes(session.id)
);
if (newPr) {
console.log(` ✅ New PR #${newPr.number} found (${newPr.head.ref})`);
return newPr;
}
console.log(` ⏳ No PR yet... polling again in 30s`);
}
throw new Error(`Timed out waiting for new PR from re-dispatched session ${session.id}`);
}
// Main: sequential merge in task order
const prMap = await findFleetPRs();
console.log(`Found ${prMap.size}/${analysis.tasks.length} fleet PRs`);
for (const [taskId, pr] of prMap) {
console.log(` ${taskId} → PR #${pr.number} (${pr.head.ref})`);
}
if (prMap.size !== analysis.tasks.length) {
console.error(`❌ Expected ${analysis.tasks.length} PRs but found ${prMap.size}. Waiting for all PRs before merging.`);
process.exit(1);
}
for (const task of analysis.tasks) {
let pr = prMap.get(task.id);
if (!pr) {
console.error(`❌ No PR found for task "${task.id}". Aborting.`);
process.exit(1);
}
let retryCount = 0;
let merged = false;
while (!merged) {
console.log(`\n📦 Processing Task "${task.id}" → PR #${pr!.number}${retryCount > 0 ? ` (retry ${retryCount})` : ""}`);
// Update branch from base before merging (skip for first PR on first attempt)
if (analysis.tasks.indexOf(task) > 0 || retryCount > 0) {
console.log(` 🔄 Updating PR #${pr!.number} branch from ${BASE_BRANCH}...`);
const updateRes = await fetch(`${API}/pulls/${pr!.number}/update-branch`, {
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
});
if (!updateRes.ok) {
const body = await updateRes.text();
if (updateRes.status === 422) {
if (retryCount >= MAX_RETRIES) {
console.error(` ❌ Conflict persists after ${MAX_RETRIES} retries. Human intervention required.`);
console.error(` PR: https://github.com/${OWNER}/${REPO}/pull/${pr!.number}`);
process.exit(1);
}
console.log(` ⚠️ Merge conflict detected. Re-dispatching task "${task.id}"...`);
pr = await redispatchTask(task, pr!);
retryCount++;
continue;
}
throw new Error(`Update branch failed (${updateRes.status}): ${body}`);
}
// Wait for the update to propagate
await new Promise(r => setTimeout(r, 5_000));
}
// Wait for CI to pass
console.log(` 🧪 Waiting for CI on PR #${pr!.number}...`);
const ciPassed = await waitForCI(pr!.number);
if (!ciPassed) {
console.error(` ❌ CI failed for PR #${pr!.number}. Aborting sequential merge.`);
process.exit(1);
}
// Merge
console.log(` ✅ CI passed. Merging PR #${pr!.number}...`);
const mergeRes = await fetch(`${API}/pulls/${pr!.number}/merge`, {
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ merge_method: "squash" }),
});
if (!mergeRes.ok) {
const body = await mergeRes.text();
console.error(` ❌ Failed to merge PR #${pr!.number}: ${body}`);
process.exit(1);
}
console.log(` 🎉 PR #${pr!.number} merged successfully.`);
merged = true;
}
}
console.log(`\n✅ All ${analysis.tasks.length} PRs merged sequentially. No conflicts.`);
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { jules } from '@google/jules-sdk'
import { analyzeIssuesPrompt } from './prompts/analyze-issues.js'
import { getIssuesAsMarkdown } from './github/markdown.js'
import { getGitRepoInfo, getCurrentBranch } from './github/git.js'
const repoInfo = await getGitRepoInfo()
const baseBranch = process.env.FLEET_BASE_BRANCH ?? await getCurrentBranch()
const issuesMarkdown = await getIssuesAsMarkdown()
const prompt = analyzeIssuesPrompt({ issuesMarkdown, repoFullName: repoInfo.fullName })
console.log(`🔍 Planning fleet for ${repoInfo.fullName} (branch: ${baseBranch})`)
const session = await jules.session({
prompt,
source: {
github: repoInfo.fullName,
baseBranch,
},
autoPr: true
})
console.log(`✅ Planner session started: ${session.id}`)
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import type { Octokit } from "@octokit/core";
/**
* Octokit plugin that caches responses using GitHub's ETag mechanism.
* 304 Not Modified responses don't count against your rate limit.
*/
export function cachePlugin(octokit: Octokit) {
const cache = new Map<string, { etag: string; data: unknown }>();
octokit.hook.wrap("request", async (request, options) => {
const key = `${options.method} ${options.url}`;
const cached = cache.get(key);
if (cached) {
(options as any).headers = {
...(options as any).headers,
"if-none-match": cached.etag,
};
}
try {
const response = await request(options);
const etag = response.headers.etag;
if (etag) {
cache.set(key, { etag, data: response.data });
}
return response;
} catch (error: any) {
if (error.status === 304 && cached) {
return { ...error.response, data: cached.data, status: 200 };
}
throw error;
}
});
}
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export interface GitRepoInfo {
owner: string;
repo: string;
/** Full GitHub path in "owner/repo" format */
fullName: string;
}
/**
* Parses the current git repository's remote URL to extract owner and repo.
* Supports both HTTPS and SSH remote URL formats.
*
* @param remoteName - The name of the remote to parse (default: "origin")
* @returns The parsed repository information
* @throws Error if not in a git repository or remote URL cannot be parsed
*
* @example
* const repo = await getGitRepoInfo();
* console.log(repo.fullName); // "owner/repo"
*/
export async function getGitRepoInfo(remoteName = "origin"): Promise<GitRepoInfo> {
const { stdout } = await execAsync(`git remote get-url ${remoteName}`);
const remoteUrl = stdout.trim();
return parseGitRemoteUrl(remoteUrl);
}
/**
* Parses a git remote URL to extract owner and repo.
* Supports both HTTPS and SSH URL formats:
* - https://github.com/owner/repo.git
* - git@github.com:owner/repo.git
*
* @param remoteUrl - The git remote URL to parse
* @returns The parsed repository information
* @throws Error if the URL format is not recognized
*/
export function parseGitRemoteUrl(remoteUrl: string): GitRepoInfo {
// SSH format: git@github.com:owner/repo.git
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(\.git)?$/);
if (sshMatch) {
const [, owner, repo] = sshMatch;
return {
owner,
repo: repo.replace(/\.git$/, ""),
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
};
}
// HTTPS format: https://github.com/owner/repo.git
const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/([^/]+)\/(.+?)(\.git)?$/);
if (httpsMatch) {
const [, owner, repo] = httpsMatch;
return {
owner,
repo: repo.replace(/\.git$/, ""),
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
};
}
throw new Error(`Unable to parse git remote URL: ${remoteUrl}`);
}
/**
* Gets the current git branch name.
*
* @returns The current branch name
* @throws Error if not in a git repository
*/
export async function getCurrentBranch(): Promise<string> {
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD");
return stdout.trim();
}
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Octokit } from "octokit";
import { cachePlugin } from "./cache-plugin.js";
import { getGitRepoInfo } from "./git.js";
/** Octokit with built-in ETag caching */
export const CachedOctokit = Octokit.plugin(cachePlugin) as typeof Octokit;
/** Fetch open issues from the current repository */
export async function getIssues(
options?: { perPage?: number; state?: "open" | "closed" | "all" }
) {
const repoInfo = await getGitRepoInfo();
const octokit = new CachedOctokit({
auth: process.env.GITHUB_TOKEN,
});
const { data } = await octokit.rest.issues.listForRepo({
owner: repoInfo.owner,
repo: repoInfo.repo,
state: options?.state ?? "open",
per_page: options?.perPage ?? 30,
});
return data.filter((issue) => !issue.pull_request);
}
export { cachePlugin } from "./cache-plugin.js";
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { getIssues } from "./issues.js";
import { getGitRepoInfo } from "./git.js";
type Issue = Awaited<ReturnType<typeof getIssues>>[number];
function toIssueMarkdown(issue: Issue): string {
const labels = issue.labels
.map((l) => (typeof l === "string" ? l : l.name))
.filter(Boolean);
const assignees = (issue.assignees ?? []).map((a) => a.login);
const reactions = issue.reactions;
const lines = [
`## #${issue.number}: ${issue.title}`,
``,
`🔗 ${issue.html_url}`,
``,
`| Field | Value |`,
`|-------|-------|`,
`| **Author** | ${issue.user?.login ?? "unknown"} |`,
`| **Association** | ${issue.author_association} |`,
`| **State** | ${issue.state}${issue.state_reason ? ` (${issue.state_reason})` : ""} |`,
`| **Locked** | ${issue.locked}${issue.active_lock_reason ? ` — ${issue.active_lock_reason}` : ""} |`,
`| **Comments** | ${issue.comments} |`,
`| **Created** | ${issue.created_at} |`,
`| **Updated** | ${issue.updated_at} |`,
];
if (issue.closed_at) {
lines.push(`| **Closed** | ${issue.closed_at} |`);
}
if (issue.closed_by) {
lines.push(`| **Closed by** | ${issue.closed_by.login} |`);
}
if (labels.length) {
lines.push(`| **Labels** | ${labels.map((l) => `\`${l}\``).join(", ")} |`);
}
if (assignees.length) {
lines.push(`| **Assignees** | ${assignees.join(", ")} |`);
}
if (issue.milestone) {
lines.push(`| **Milestone** | ${issue.milestone.title} |`);
}
if (issue.draft) {
lines.push(`| **Draft** | true |`);
}
if (issue.pull_request) {
lines.push(`| **Type** | Pull Request |`);
}
if (reactions) {
const rxn = [
reactions["+1"] && `👍 ${reactions["+1"]}`,
reactions["-1"] && `👎 ${reactions["-1"]}`,
reactions.laugh && `😄 ${reactions.laugh}`,
reactions.hooray && `🎉 ${reactions.hooray}`,
reactions.confused && `😕 ${reactions.confused}`,
reactions.heart && `❤️ ${reactions.heart}`,
reactions.rocket && `🚀 ${reactions.rocket}`,
reactions.eyes && `👀 ${reactions.eyes}`,
].filter(Boolean);
if (rxn.length) {
lines.push(`| **Reactions** | ${rxn.join(" ")} |`);
}
}
lines.push(``);
if (issue.body) {
lines.push(`### Description`, ``, issue.body.trim(), ``);
}
lines.push(`---`, ``);
return lines.join("\n");
}
async function toIssueDocMarkdown(issues: Issue[]) {
const repoInfo = await getGitRepoInfo();
const lines = [
`# Open Issues — ${repoInfo.fullName}`,
``,
`> ${issues.length} issues fetched on ${new Date().toISOString()}`,
``,
`---`,
``,
...issues.map(toIssueMarkdown),
];
return lines.join("\n");
}
export async function getIssuesAsMarkdown() {
const issues = await getIssues();
return toIssueDocMarkdown(issues);
}// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import type { AnalyzeIssuesPromptOptions } from "../types.js";
export function analyzeIssuesPrompt({
issuesMarkdown,
repoFullName,
}: AnalyzeIssuesPromptOptions): string {
const now = new Date();
const YYYY_MM_DD = `${now.getFullYear()}_${String(now.getMonth() + 1).padStart(2, "0")}_${String(now.getDate()).padStart(2, "0")}`;
return `Analyze ${repoFullName} open issues and produce implementation tasks.
You are a senior software engineer performing deep technical triage on GitHub issues from a single repository. You have access to the full codebase. Your job is not just to classify issues — it is to diagnose root causes at the code level, propose concrete implementations, and produce task prompts detailed enough that another engineer could start coding immediately.
## Your Input
Below is a markdown document containing all open issues for **${repoFullName}**. Each issue includes its number, title, author, labels, timestamps, and full description.
## Issues to analyze
${issuesMarkdown}
## Your Task
Perform a four-phase analysis: **Investigate**, **Architect**, **Plan**, and **Dispatch**
---
### Phase 1: Investigate
For each issue, trace the reported behavior to its source in the codebase. Produce a **code-level diagnosis**, not a summary.
For each issue you must:
1. **Identify the exact code path** that causes the reported behavior. Reference specific files, functions, and line ranges.
2. **Explain the mechanism** — why does this code produce this symptom? Show the relevant code snippet and annotate what goes wrong.
3. **Determine the root cause category**: Is this a bug, a missing feature, an architectural gap, error handling omission, race condition, or documentation gap?
Example of the depth expected:
\`\`\`markdown
### Issue #19: Streaming 404 after session creation
**Code path:** \\\`src/session.ts → stream() → fetchActivities() → GET /sessions/{id}/activities\\\`
**Mechanism:** When \\\`stream()\\\` is called immediately after session creation, the activities endpoint hasn't been provisioned yet. The current implementation in \\\`fetchActivities()\\\` makes a single request with no retry logic:
\\\`\\\`\\\`typescript
// src/activities.ts:42-48
async function fetchActivities(sessionId: string) {
const response = await fetch(\\\`\\\${BASE_URL}/sessions/\\\${sessionId}/activities\\\`);
if (!response.ok) {
throw new ApiError(response.status, await response.json()); // ← throws immediately on 404
}
return response.json();
}
\\\`\\\`\\\`
The 404 is not a "real" error — it's a timing issue. The session exists (creation returned 200) but the activities sub-resource has eventual consistency.
**Root cause:** Missing retry-with-backoff for transient 404s in the activity streaming pipeline.
\`\`\`
After investigating each issue individually, **cross-reference** them to find issues that share the same root cause or code path. Group related issues together.
---
### Phase 2: Architect
For each root cause group, design a **concrete solution** with implementation details. This is not "add better error handling" — this is "here is the function signature, the retry logic, and how it integrates."
For each solution you must provide:
1. **Proposed implementation** — actual TypeScript/code showing the solution. This should be close to production-ready, not pseudocode.
2. **Integration points** — exactly where in the existing code this gets wired in, with before/after snippets.
3. **Edge cases and risks** — what could go wrong, what assumptions you're making.
4. **Test scenarios** — specific test cases that validate the fix.
Example of the depth expected:
\`\`\`markdown
### Solution: Retry-aware activity streaming
**Files modified:** \\\`src/activities.ts\\\`, \\\`src/retry.ts\\\` (new)
**Implementation:**
\\\`\\\`\\\`typescript
// NEW: src/retry.ts
interface RetryOptions {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
retryOn: (status: number) => boolean;
}
async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < options.maxAttempts; attempt++) {
try {
return await fn();
} catch (err: any) {
lastError = err;
if (!options.retryOn(err.status)) throw err;
const delay = Math.min(
options.baseDelayMs * Math.pow(2, attempt),
options.maxDelayMs
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastError;
}
\\\`\\\`\\\`
**Integration (before → after):**
\\\`\\\`\\\`diff
// src/activities.ts
- async function fetchActivities(sessionId: string) {
- const response = await fetch(\\\`\\\${BASE_URL}/sessions/\\\${sessionId}/activities\\\`);
- if (!response.ok) throw new ApiError(response.status, await response.json());
- return response.json();
- }
+ async function fetchActivities(sessionId: string) {
+ return withRetry(
+ async () => {
+ const response = await fetch(\\\`\\\${BASE_URL}/sessions/\\\${sessionId}/activities\\\`);
+ if (!response.ok) throw new ApiError(response.status, await response.json());
+ return response.json();
+ },
+ { maxAttempts: 10, baseDelayMs: 1000, maxDelayMs: 30000, retryOn: (s) => s === 404 }
+ );
+ }
\\\`\\\`\\\`
**Test scenarios:**
1. Activity endpoint returns 404 three times then 200 → stream yields activities
2. Activity endpoint returns 404 for all attempts → throws after max retries
3. Activity endpoint returns 500 → throws immediately (not retried)
4. Activity endpoint returns 200 immediately → no retry delay
\`\`\`
---
### Phase 3: Plan
Produce two files in the target repository:
- \`.fleet/${YYYY_MM_DD}/issue_tasks.md\`
- \`.fleet/${YYYY_MM_DD}/issue_tasks.json\`
#### Merge conflict avoidance rule
These tasks will be executed as **parallel agents**, each creating a separate PR against the same branch. If two tasks modify the same file, they **will** create merge conflicts. Therefore:
- **No two tasks may modify the same file, including test files.** If two root causes require changes to the same source file or test file, merge them into one task.
- For each source file in a task, identify its corresponding test file(s) and include them in the ownership matrix.
- Produce a **File Ownership Matrix** showing exactly which task owns which source and test files. Verify no file appears twice.
#### Coupling analysis
Before finalizing tasks, check for **implicitly coupled files** — files not directly in a task's file list but tightly coupled to it:
- **Test files** that exercise code from multiple tasks (e.g., a shared integration test, a test file with a shared mock server)
- **Barrel exports** (\`index.ts\`) that re-export from files owned by different tasks
- **Shared utilities** imported by files in different tasks
If any coupled file appears in more than one task's dependency cone, **merge those tasks into one.** It is better to have fewer, larger tasks than to risk merge conflicts.
#### issue_tasks.md structure
\`\`\`markdown
# Issue Analysis: ${repoFullName}
> Analyzed N issues on ${now.toISOString()}
## Executive Summary
[2-3 sentences: how many root causes found, how many are addressable, overall health assessment]
## Root Cause Analysis
### RC-1: [Root cause title]
**Related issues:** #X, #Y, #Z
**Severity:** Critical / High / Medium / Low
**Files involved:** \\\`src/file.ts\\\`, \\\`src/other.ts\\\`
#### Diagnosis
[Code-level explanation with snippets showing the problematic code path]
#### Proposed Solution
[Full implementation with code, diffs, integration points as described in Phase 2]
#### Test Plan
[Specific test scenarios with inputs and expected outputs]
---
### RC-2: [Root cause title]
...
## Task Plan
| # | Task | Root Cause | Issues | Files | Risk |
|---|------|-----------|--------|-------|------|
| 1 | [title] | RC-1 | #X, #Y | \\\`src/a.ts\\\`, \\\`src/b.ts\\\` | Medium |
| 2 | [title] | RC-2 | #Z | \\\`src/c.ts\\\` | Low |
## File Ownership Matrix
| File | Task | Change Type |
|------|------|-------------|
| \\\`src/a.ts\\\` | 1 | Modify |
| \\\`src/b.ts\\\` | 1 | Modify |
| \\\`src/retry.ts\\\` | 1 | Create |
| \\\`tests/a.test.ts\\\` | 1 | Modify |
| \\\`src/c.ts\\\` | 2 | Modify |
| \\\`tests/c.test.ts\\\` | 2 | Modify |
## Unaddressable Issues
Issues that require changes outside this repository (backend API, infrastructure, product decisions):
| Issue | Reason | Suggested Owner |
|-------|--------|-----------------|
| #18 | Requires backend API to support \\\`requireApproval: false\\\` | Backend team |
\`\`\`
#### issue_tasks.json schema
\`\`\`json
{
"repo": "${repoFullName}",
"analyzed_at": "ISO-8601 timestamp",
"root_causes": [
{
"id": "rc-kebab-id",
"title": "Human readable title",
"severity": "critical | high | medium | low",
"issues": [19, 23],
"files": ["src/polling.ts", "src/session.ts"],
"description": "Brief explanation of root cause",
"solution_summary": "Brief description of the proposed fix approach"
}
],
"tasks": [
{
"id": "task-kebab-id",
"title": "Human readable task title",
"root_cause": "rc-kebab-id",
"issues": [19, 23],
"files": ["src/polling.ts", "src/session.ts"],
"new_files": ["src/retry.ts"],
"test_files": ["tests/polling.test.ts", "tests/session.test.ts"],
"risk": "low | medium | high",
"prompt": "A highly detailed, code-rich, self-contained prompt for a coding agent. This prompt must include: 1. The exact files to modify and create 2. The exact test files to modify (and ONLY these test files) 3. The root cause explanation with relevant code snippets from the current codebase 4. The proposed implementation with full code examples 5. Before/after diffs showing the integration 6. Test scenarios with expected behavior 7. Acceptance criteria the PR must meet 8. A FILE BOUNDARY rule: 'You may ONLY modify the files listed above. If a test file outside your boundary fails, you must make your source changes backward-compatible so the existing test passes unmodified. Do NOT rename, move, or delete any files outside your boundary.' The agent receiving this prompt has full repo access but no context about other tasks. Include everything it needs."
}
],
"unaddressable": [
{
"issue": 18,
"reason": "Requires backend API change — FAILED_PRECONDITION is server-side enforcement",
"suggested_owner": "Backend team"
}
],
"file_ownership": {
"src/polling.ts": "task-kebab-id",
"src/session.ts": "task-kebab-id",
"src/retry.ts": "task-kebab-id",
"tests/polling.test.ts": "task-kebab-id",
"tests/session.test.ts": "task-kebab-id"
}
}
\`\`\`
## Critical Rules
1. **Show your work in code.** Every diagnosis must reference specific files, functions, and line ranges. Every solution must include implementation code, not descriptions.
2. **Never split a file across tasks.** If two issues need changes to the same file, combine them into one task.
3. **Task prompts must be code-rich and self-contained.** Each prompt is sent to a coding agent that has repo access but zero context about the analysis. Include code snippets, diffs, function signatures, and acceptance criteria.
4. **Use exact file paths** from the repository. Do not guess paths.
5. **Mark issues as unaddressable** if they require changes outside this repository's control. Provide the reasoning and the suggested owner.
6. **Order tasks by risk** — lowest risk first, so easy wins merge before complex changes.
7. **Diffs must be valid.** Before/after code must reflect the actual current codebase, not approximations.
8. **Test files must be in the ownership matrix.** For every source file a task modifies, include its corresponding test file(s) in both the task's \`test_files\` array and the \`file_ownership\` map. No test file may appear in more than one task.
9. **Test boundary: tasks must not touch files outside their ownership.** Each task's prompt must instruct the agent to ONLY modify files listed in its \`files\`, \`new_files\`, and \`test_files\` arrays. If a test outside the boundary fails, the agent must make its implementation backward-compatible rather than modifying the unowned test.
---
## Phase 4: Dispatch
After you have written both \`.fleet/${YYYY_MM_DD}/issue_tasks.md\` and \`.fleet/${YYYY_MM_DD}/issue_tasks.json\`, run the dispatcher to spawn parallel Jules sessions for each task:
\`\`\`bash
bun run scripts/fleet/fleet-dispatch.ts
\`\`\`
This script reads the \`tasks\` array from the JSON file, creates a Jules session for each task using its \`prompt\` field, and logs the resulting session IDs. **Do not skip this step** — the analysis is only complete once the sub-tasks have been dispatched.`;
}
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import type { AnalyzeIssuesPromptOptions } from "../types.js";
import { analyzeIssuesPrompt } from "./analyze-issues.js";
import { getGitRepoInfo } from "../github/git.js";
/**
* Static bootstrap prompt for scheduled Jules sessions.
*
* Since the Jules scheduled session receives a fixed prompt (issues aren't
* known at scheduling time), this prompt instructs Jules to:
* 1. Run `bun run scripts/fleet/fleet-analyze.ts` to fetch current open issues
* 2. Use the output as input for the analyze-issues prompt
* 3. Run the dispatcher to dispatch sub-tasks
*
* Usage: Pass this prompt's output as the `prompt` field when creating
* a scheduled Jules session.
*/
export async function bootstrapPrompt(): Promise<string> {
const repoInfo = await getGitRepoInfo();
// We generate the analysis prompt template with a placeholder.
// Jules will replace it with the actual issues at runtime.
const analysisPrompt = analyzeIssuesPrompt({
issuesMarkdown: "{{ISSUES_MARKDOWN}}",
repoFullName: repoInfo.fullName,
});
return `You are the fleet planner for ${repoInfo.fullName}. Your job is to fetch open GitHub issues, analyze them, and dispatch parallel coding agents.
## Step 1: Fetch open issues
Run this command to get the current open issues as markdown:
\`\`\`bash
bun run scripts/fleet/fleet-analyze.ts
\`\`\`
Capture the entire output. This is the issues document you will analyze.
## Step 2: Analyze issues and produce tasks
Using the issues output from Step 1, perform the full analysis described below. Everywhere you see \`{{ISSUES_MARKDOWN}}\` in the analysis prompt, substitute the output from Step 1.
---
${analysisPrompt}
---
## Step 3: Dispatch
After writing both \`.fleet/\` files, run the dispatcher:
\`\`\`bash
bun run scripts/fleet/fleet-dispatch.ts
\`\`\`
This dispatches parallel Jules sessions for each task and logs session IDs to \`.fleet/{date}/sessions.json\`.
**Do not skip any step. The analysis is only complete once sub-tasks have been dispatched.**`;
}
#!/bin/bash
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Setup script for the automate-github-issues skill
set -e
echo "🔧 Setting up automate-github-issues skill..."
echo ""
# Check for Bun
if command -v bun &> /dev/null; then
echo "✅ Bun found: $(bun --version)"
else
echo "⚠️ Bun not found. Installing..."
curl -fsSL https://bun.sh/install | bash
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
echo "✅ Bun installed: $(bun --version)"
fi
echo ""
# Install dependencies
echo "📦 Installing dependencies..."
bun install
echo "✅ Dependencies installed."
echo ""
# Scaffold .env if it doesn't exist
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
ENV_FILE="$SKILL_DIR/.env"
ENV_EXAMPLE="$SKILL_DIR/assets/.env.example"
if [ ! -f "$ENV_FILE" ]; then
if [ -f "$ENV_EXAMPLE" ]; then
cp "$ENV_EXAMPLE" "$ENV_FILE"
echo "📝 Created .env from template. Edit it with your API keys."
else
echo "⚠️ No .env.example found. Create .env manually with JULES_API_KEY and GITHUB_TOKEN."
fi
else
echo "✅ .env already exists."
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 Next steps (manual):"
echo ""
echo " 1. Edit .env with your API keys:"
echo " JULES_API_KEY=your-key-here"
echo " GITHUB_TOKEN=your-token-here"
echo ""
echo " 2. Add GitHub Actions workflows:"
echo " cp assets/fleet-dispatch.yml .github/workflows/"
echo " cp assets/fleet-merge.yml .github/workflows/"
echo ""
echo " 3. Add secrets to your GitHub repo:"
echo " Settings → Secrets → Actions → New repository secret"
echo " - JULES_API_KEY"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export interface IssueAnalysis {
repo: string;
analyzed_at: string;
root_causes: RootCause[];
tasks: Task[];
unaddressable: UnaddressableIssue[];
file_ownership: Record<string, string>;
}
export interface RootCause {
id: string;
title: string;
severity: "critical" | "high" | "medium" | "low";
issues: number[];
files: string[];
description: string;
solution_summary: string;
}
export interface Task {
id: string;
title: string;
root_cause: string;
issues: number[];
files: string[];
new_files: string[];
test_files: string[];
risk: "low" | "medium" | "high";
prompt: string;
}
export interface UnaddressableIssue {
issue: number;
reason: string;
suggested_owner: string;
}
export interface AnalyzeIssuesPromptOptions {
issuesMarkdown: string;
repoFullName: string;
}
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist",
"types": ["bun"]
},
"include": ["scripts/**/*.ts"]
}
Related skills
FAQ
What environment variables does automate-github-issues require?
automate-github-issues requires JULES_API_KEY to dispatch Jules sessions and GITHUB_TOKEN with repository access for fetching issues and merging PRs. FLEET_BASE_BRANCH optionally overrides the default base branch for fleet runs.
What does automate-github-issues do with open GitHub issues?
automate-github-issues schedules Jules fleet sessions that fetch open GitHub issues and dispatch parallel coding agents to work them. The skill removes manual triage when fleet automation is configured with valid API credentials.
Is Automate Github Issues safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.