@google/jules-fleet 0.0.1-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -0
- package/dist/analyze/formatting.d.ts +19 -0
- package/dist/analyze/goals.d.ts +18 -0
- package/dist/analyze/handler.d.ts +23 -0
- package/dist/analyze/index.d.ts +8 -0
- package/dist/analyze/milestone.d.ts +43 -0
- package/dist/analyze/prompt.d.ts +10 -0
- package/dist/analyze/spec.d.ts +54 -0
- package/dist/analyze/triage-prompt.d.ts +16 -0
- package/dist/cli/analyze.command.d.ts +24 -0
- package/dist/cli/analyze.command.mjs +1015 -0
- package/dist/cli/commands.json +1 -0
- package/dist/cli/configure.command.d.ts +21 -0
- package/dist/cli/configure.command.mjs +623 -0
- package/dist/cli/dispatch.command.d.ts +16 -0
- package/dist/cli/dispatch.command.mjs +777 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.mjs +40 -0
- package/dist/cli/init.command.d.ts +38 -0
- package/dist/cli/init.command.mjs +1287 -0
- package/dist/cli/merge.command.d.ts +36 -0
- package/dist/cli/merge.command.mjs +859 -0
- package/dist/cli/signal.command.d.ts +2 -0
- package/dist/cli/signal.command.mjs +288 -0
- package/dist/configure/handler.d.ts +19 -0
- package/dist/configure/index.d.ts +4 -0
- package/dist/configure/labels.d.ts +6 -0
- package/dist/configure/spec.d.ts +49 -0
- package/dist/dispatch/events.d.ts +12 -0
- package/dist/dispatch/handler.d.ts +21 -0
- package/dist/dispatch/index.d.ts +5 -0
- package/dist/dispatch/spec.d.ts +47 -0
- package/dist/dispatch/status.d.ts +24 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +2105 -0
- package/dist/init/handler.d.ts +22 -0
- package/dist/init/index.d.ts +4 -0
- package/dist/init/ops/commit-files.d.ts +10 -0
- package/dist/init/ops/create-branch.d.ts +16 -0
- package/dist/init/ops/create-pr.d.ts +15 -0
- package/dist/init/ops/pr-body.d.ts +5 -0
- package/dist/init/ops/upload-secrets.d.ts +11 -0
- package/dist/init/spec.d.ts +50 -0
- package/dist/init/templates/analyze.d.ts +2 -0
- package/dist/init/templates/dispatch.d.ts +2 -0
- package/dist/init/templates/example-goal.d.ts +5 -0
- package/dist/init/templates/merge.d.ts +2 -0
- package/dist/init/templates/types.d.ts +6 -0
- package/dist/init/templates.d.ts +10 -0
- package/dist/init/types.d.ts +19 -0
- package/dist/init/wizard/headless.d.ts +8 -0
- package/dist/init/wizard/index.d.ts +3 -0
- package/dist/init/wizard/interactive.d.ts +9 -0
- package/dist/init/wizard/types.d.ts +22 -0
- package/dist/merge/handler.d.ts +21 -0
- package/dist/merge/index.d.ts +5 -0
- package/dist/merge/ops/index.d.ts +4 -0
- package/dist/merge/ops/redispatch.d.ts +8 -0
- package/dist/merge/ops/squash-merge.d.ts +8 -0
- package/dist/merge/ops/update-branch.d.ts +11 -0
- package/dist/merge/ops/wait-for-ci.d.ts +7 -0
- package/dist/merge/select/by-fleet-run.d.ts +8 -0
- package/dist/merge/select/by-label.d.ts +7 -0
- package/dist/merge/select/index.d.ts +2 -0
- package/dist/merge/spec.d.ts +99 -0
- package/dist/shared/auth/cache-plugin.d.ts +9 -0
- package/dist/shared/auth/git.d.ts +22 -0
- package/dist/shared/auth/index.d.ts +4 -0
- package/dist/shared/auth/octokit.d.ts +11 -0
- package/dist/shared/auth/resolve-key.d.ts +11 -0
- package/dist/shared/events/analyze.d.ts +37 -0
- package/dist/shared/events/configure.d.ts +21 -0
- package/dist/shared/events/dispatch.d.ts +26 -0
- package/dist/shared/events/error.d.ts +7 -0
- package/dist/shared/events/index.d.ts +16 -0
- package/dist/shared/events/init.d.ts +49 -0
- package/dist/shared/events/merge.d.ts +72 -0
- package/dist/shared/events.d.ts +1 -0
- package/dist/shared/index.d.ts +6 -0
- package/dist/shared/result/create-result-schemas.d.ts +72 -0
- package/dist/shared/result/fail.d.ts +10 -0
- package/dist/shared/result/index.d.ts +3 -0
- package/dist/shared/result/ok.d.ts +5 -0
- package/dist/shared/schemas/check-run.d.ts +16 -0
- package/dist/shared/schemas/index.d.ts +4 -0
- package/dist/shared/schemas/label.d.ts +16 -0
- package/dist/shared/schemas/pr.d.ts +19 -0
- package/dist/shared/schemas/repo-info.d.ts +16 -0
- package/dist/shared/session-dispatcher.d.ts +18 -0
- package/dist/shared/ui/assert-never.d.ts +13 -0
- package/dist/shared/ui/index.d.ts +18 -0
- package/dist/shared/ui/interactive.d.ts +19 -0
- package/dist/shared/ui/plain.d.ts +16 -0
- package/dist/shared/ui/render/analyze.d.ts +4 -0
- package/dist/shared/ui/render/configure.d.ts +4 -0
- package/dist/shared/ui/render/dispatch.d.ts +4 -0
- package/dist/shared/ui/render/error.d.ts +4 -0
- package/dist/shared/ui/render/init.d.ts +4 -0
- package/dist/shared/ui/render/merge.d.ts +4 -0
- package/dist/shared/ui/session-url.d.ts +13 -0
- package/dist/shared/ui/spec.d.ts +30 -0
- package/dist/signal/handler.d.ts +17 -0
- package/dist/signal/index.d.ts +3 -0
- package/dist/signal/spec.d.ts +60 -0
- package/package.json +76 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2105 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/shared/result/create-result-schemas.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
function createResultSchemas(dataSchema, errorCodeSchema) {
|
|
7
|
+
const Success = z.object({
|
|
8
|
+
success: z.literal(true),
|
|
9
|
+
data: dataSchema
|
|
10
|
+
});
|
|
11
|
+
const Failure = z.object({
|
|
12
|
+
success: z.literal(false),
|
|
13
|
+
error: z.object({
|
|
14
|
+
code: errorCodeSchema,
|
|
15
|
+
message: z.string(),
|
|
16
|
+
suggestion: z.string().optional(),
|
|
17
|
+
recoverable: z.boolean()
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
return { Success, Failure };
|
|
21
|
+
}
|
|
22
|
+
// src/shared/result/ok.ts
|
|
23
|
+
function ok(data) {
|
|
24
|
+
return { success: true, data };
|
|
25
|
+
}
|
|
26
|
+
// src/shared/result/fail.ts
|
|
27
|
+
function fail(code, message, recoverable = false, suggestion) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: { code, message, recoverable, suggestion }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// src/shared/schemas/repo-info.ts
|
|
34
|
+
import { z as z2 } from "zod";
|
|
35
|
+
var RepoInfoSchema = z2.object({
|
|
36
|
+
owner: z2.string().min(1),
|
|
37
|
+
repo: z2.string().min(1),
|
|
38
|
+
fullName: z2.string().regex(/^[^/]+\/[^/]+$/, "Must be in owner/repo format")
|
|
39
|
+
});
|
|
40
|
+
// src/shared/schemas/pr.ts
|
|
41
|
+
import { z as z3 } from "zod";
|
|
42
|
+
var PRSchema = z3.object({
|
|
43
|
+
number: z3.number().positive(),
|
|
44
|
+
headRef: z3.string(),
|
|
45
|
+
headSha: z3.string(),
|
|
46
|
+
body: z3.string().nullable()
|
|
47
|
+
});
|
|
48
|
+
// src/shared/schemas/check-run.ts
|
|
49
|
+
import { z as z4 } from "zod";
|
|
50
|
+
var CheckRunSchema = z4.object({
|
|
51
|
+
name: z4.string(),
|
|
52
|
+
status: z4.string(),
|
|
53
|
+
conclusion: z4.string().nullable()
|
|
54
|
+
});
|
|
55
|
+
// src/shared/schemas/label.ts
|
|
56
|
+
import { z as z5 } from "zod";
|
|
57
|
+
var LabelSchema = z5.object({
|
|
58
|
+
name: z5.string().min(1),
|
|
59
|
+
color: z5.string().regex(/^[0-9a-fA-F]{6}$/),
|
|
60
|
+
description: z5.string()
|
|
61
|
+
});
|
|
62
|
+
// src/shared/auth/octokit.ts
|
|
63
|
+
import { Octokit } from "octokit";
|
|
64
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
65
|
+
|
|
66
|
+
// src/shared/auth/cache-plugin.ts
|
|
67
|
+
function cachePlugin(octokit) {
|
|
68
|
+
const cache = new Map;
|
|
69
|
+
octokit.hook.wrap("request", async (request, options) => {
|
|
70
|
+
const key = `${options.method} ${options.url}`;
|
|
71
|
+
const cached = cache.get(key);
|
|
72
|
+
if (cached) {
|
|
73
|
+
options.headers = {
|
|
74
|
+
...options.headers,
|
|
75
|
+
"if-none-match": cached.etag
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const response = await request(options);
|
|
80
|
+
const etag = response.headers.etag;
|
|
81
|
+
if (etag) {
|
|
82
|
+
cache.set(key, { etag, data: response.data });
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.status === 304 && cached) {
|
|
87
|
+
return { ...error.response, data: cached.data, status: 200 };
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/shared/auth/resolve-key.ts
|
|
95
|
+
function resolvePrivateKey(base64Value, rawValue) {
|
|
96
|
+
if (base64Value) {
|
|
97
|
+
return Buffer.from(base64Value, "base64").toString("utf-8");
|
|
98
|
+
}
|
|
99
|
+
if (rawValue) {
|
|
100
|
+
return rawValue.replace(/\\n/g, `
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
throw new Error("No private key provided. Set GITHUB_APP_PRIVATE_KEY_BASE64 (recommended) or GITHUB_APP_PRIVATE_KEY.");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/shared/auth/octokit.ts
|
|
107
|
+
var CachedOctokit = Octokit.plugin(cachePlugin);
|
|
108
|
+
function getAuthOptions() {
|
|
109
|
+
const appId = process.env.GITHUB_APP_ID;
|
|
110
|
+
const privateKeyBase64 = process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
|
|
111
|
+
const privateKeyRaw = process.env.GITHUB_APP_PRIVATE_KEY;
|
|
112
|
+
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
113
|
+
if (appId && (privateKeyBase64 || privateKeyRaw) && installationId) {
|
|
114
|
+
return {
|
|
115
|
+
authStrategy: createAppAuth,
|
|
116
|
+
auth: {
|
|
117
|
+
appId,
|
|
118
|
+
privateKey: resolvePrivateKey(privateKeyBase64, privateKeyRaw),
|
|
119
|
+
installationId: Number(installationId)
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const token = process.env.GITHUB_TOKEN;
|
|
124
|
+
if (token) {
|
|
125
|
+
return { auth: token };
|
|
126
|
+
}
|
|
127
|
+
throw new Error("GitHub auth not configured. Set GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY + GITHUB_APP_INSTALLATION_ID for App auth, or GITHUB_TOKEN for PAT auth.");
|
|
128
|
+
}
|
|
129
|
+
function createFleetOctokit() {
|
|
130
|
+
return new CachedOctokit(getAuthOptions());
|
|
131
|
+
}
|
|
132
|
+
// src/shared/auth/git.ts
|
|
133
|
+
import { exec } from "child_process";
|
|
134
|
+
import { promisify } from "util";
|
|
135
|
+
var execAsync = promisify(exec);
|
|
136
|
+
async function getGitRepoInfo(remoteName = "origin") {
|
|
137
|
+
const ghRepo = process.env.GITHUB_REPOSITORY;
|
|
138
|
+
if (ghRepo) {
|
|
139
|
+
const [owner, repo] = ghRepo.split("/");
|
|
140
|
+
return { owner, repo, fullName: ghRepo };
|
|
141
|
+
}
|
|
142
|
+
const { stdout } = await execAsync(`git remote get-url ${remoteName}`);
|
|
143
|
+
return parseGitRemoteUrl(stdout.trim());
|
|
144
|
+
}
|
|
145
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
146
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(\.git)?$/);
|
|
147
|
+
if (sshMatch) {
|
|
148
|
+
const [, owner, repo] = sshMatch;
|
|
149
|
+
return {
|
|
150
|
+
owner,
|
|
151
|
+
repo: repo.replace(/\.git$/, ""),
|
|
152
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/([^/]+)\/(.+?)(\.git)?$/);
|
|
156
|
+
if (httpsMatch) {
|
|
157
|
+
const [, owner, repo] = httpsMatch;
|
|
158
|
+
return {
|
|
159
|
+
owner,
|
|
160
|
+
repo: repo.replace(/\.git$/, ""),
|
|
161
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Unable to parse git remote URL: ${remoteUrl}`);
|
|
165
|
+
}
|
|
166
|
+
async function getCurrentBranch() {
|
|
167
|
+
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD");
|
|
168
|
+
return stdout.trim();
|
|
169
|
+
}
|
|
170
|
+
// src/shared/ui/interactive.ts
|
|
171
|
+
import * as p from "@clack/prompts";
|
|
172
|
+
|
|
173
|
+
// src/shared/ui/render/init.ts
|
|
174
|
+
function renderInitEvent(event, ctx) {
|
|
175
|
+
switch (event.type) {
|
|
176
|
+
case "init:start":
|
|
177
|
+
ctx.info(`Initializing fleet for ${event.owner}/${event.repo}`);
|
|
178
|
+
break;
|
|
179
|
+
case "init:branch:creating":
|
|
180
|
+
ctx.startSpinner(`Creating branch ${event.name} from ${event.base}`);
|
|
181
|
+
break;
|
|
182
|
+
case "init:branch:created":
|
|
183
|
+
ctx.stopSpinner(`Branch ${event.name} created`);
|
|
184
|
+
break;
|
|
185
|
+
case "init:file:committed":
|
|
186
|
+
ctx.info(` ✓ ${event.path}`);
|
|
187
|
+
break;
|
|
188
|
+
case "init:file:skipped":
|
|
189
|
+
ctx.warn(` ⊘ ${event.path} — ${event.reason}`);
|
|
190
|
+
break;
|
|
191
|
+
case "init:pr:creating":
|
|
192
|
+
ctx.startSpinner("Creating pull request…");
|
|
193
|
+
break;
|
|
194
|
+
case "init:pr:created":
|
|
195
|
+
ctx.stopSpinner(`PR #${event.number} created`);
|
|
196
|
+
ctx.info(` ${event.url}`);
|
|
197
|
+
break;
|
|
198
|
+
case "init:done":
|
|
199
|
+
ctx.success(`Fleet initialized — PR: ${event.prUrl}`);
|
|
200
|
+
break;
|
|
201
|
+
case "init:auth:detected":
|
|
202
|
+
ctx.success(`Auth: ${event.method === "token" ? "GITHUB_TOKEN" : "GitHub App"}`);
|
|
203
|
+
break;
|
|
204
|
+
case "init:secret:uploading":
|
|
205
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
206
|
+
break;
|
|
207
|
+
case "init:secret:uploaded":
|
|
208
|
+
ctx.stopSpinner(`Secret ${event.name} saved`);
|
|
209
|
+
break;
|
|
210
|
+
case "init:secret:skipped":
|
|
211
|
+
ctx.warn(` ⊘ ${event.name} — ${event.reason}`);
|
|
212
|
+
break;
|
|
213
|
+
case "init:dry-run":
|
|
214
|
+
ctx.info("Would create:");
|
|
215
|
+
event.files.forEach((f) => ctx.message(` ${f}`));
|
|
216
|
+
break;
|
|
217
|
+
case "init:already-initialized":
|
|
218
|
+
ctx.warn("Repository is already initialized");
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/shared/ui/render/configure.ts
|
|
224
|
+
function renderConfigureEvent(event, ctx) {
|
|
225
|
+
switch (event.type) {
|
|
226
|
+
case "configure:start":
|
|
227
|
+
ctx.info(`Configuring ${event.resource} for ${event.owner}/${event.repo}`);
|
|
228
|
+
break;
|
|
229
|
+
case "configure:label:created":
|
|
230
|
+
ctx.info(` ✓ Label "${event.name}" created`);
|
|
231
|
+
break;
|
|
232
|
+
case "configure:label:exists":
|
|
233
|
+
ctx.warn(` ⊘ Label "${event.name}" already exists`);
|
|
234
|
+
break;
|
|
235
|
+
case "configure:secret:uploading":
|
|
236
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
237
|
+
break;
|
|
238
|
+
case "configure:secret:uploaded":
|
|
239
|
+
ctx.stopSpinner(`Secret ${event.name} uploaded`);
|
|
240
|
+
break;
|
|
241
|
+
case "configure:done":
|
|
242
|
+
ctx.success("Configuration complete");
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/shared/ui/session-url.ts
|
|
248
|
+
var JULES_BASE_URL = "https://jules.google.com";
|
|
249
|
+
function sessionUrl(sessionId) {
|
|
250
|
+
return `${JULES_BASE_URL}/sessions/${sessionId}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/shared/ui/render/analyze.ts
|
|
254
|
+
function renderAnalyzeEvent(event, ctx) {
|
|
255
|
+
switch (event.type) {
|
|
256
|
+
case "analyze:start":
|
|
257
|
+
ctx.info(`Analyzing ${event.goalCount} goal(s) for ${event.owner}/${event.repo}`);
|
|
258
|
+
break;
|
|
259
|
+
case "analyze:goal:start":
|
|
260
|
+
if (event.total > 1) {
|
|
261
|
+
ctx.step(`[${event.index}/${event.total}] ${event.file}`);
|
|
262
|
+
} else {
|
|
263
|
+
ctx.step(event.file);
|
|
264
|
+
}
|
|
265
|
+
if (event.milestone)
|
|
266
|
+
ctx.info(` Milestone: ${event.milestone}`);
|
|
267
|
+
break;
|
|
268
|
+
case "analyze:milestone:resolved":
|
|
269
|
+
ctx.info(` Milestone "${event.title}" (#${event.id})`);
|
|
270
|
+
break;
|
|
271
|
+
case "analyze:context:fetched":
|
|
272
|
+
ctx.info(` Context: ${event.openIssues} open, ${event.closedIssues} closed, ${event.prs} PRs`);
|
|
273
|
+
break;
|
|
274
|
+
case "analyze:session:dispatching":
|
|
275
|
+
ctx.startSpinner(`Dispatching session for ${event.goal}…`);
|
|
276
|
+
break;
|
|
277
|
+
case "analyze:session:started":
|
|
278
|
+
ctx.stopSpinner(`Session started: ${event.id}`);
|
|
279
|
+
ctx.info(` ${sessionUrl(event.id)}`);
|
|
280
|
+
break;
|
|
281
|
+
case "analyze:session:failed":
|
|
282
|
+
ctx.stopSpinner();
|
|
283
|
+
ctx.error(` Failed: ${event.error}`);
|
|
284
|
+
break;
|
|
285
|
+
case "analyze:done":
|
|
286
|
+
ctx.success(`Analysis complete — ${event.sessionsStarted} session(s) from ${event.goalsProcessed} goal(s)`);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/shared/ui/render/dispatch.ts
|
|
292
|
+
function renderDispatchEvent(event, ctx) {
|
|
293
|
+
switch (event.type) {
|
|
294
|
+
case "dispatch:start":
|
|
295
|
+
ctx.info(`Dispatching from milestone ${event.milestone}`);
|
|
296
|
+
break;
|
|
297
|
+
case "dispatch:scanning":
|
|
298
|
+
ctx.startSpinner("Scanning for fleet issues…");
|
|
299
|
+
break;
|
|
300
|
+
case "dispatch:found":
|
|
301
|
+
ctx.stopSpinner(`Found ${event.count} undispatched issue(s)`);
|
|
302
|
+
break;
|
|
303
|
+
case "dispatch:issue:dispatching":
|
|
304
|
+
ctx.startSpinner(`#${event.number}: ${event.title}`);
|
|
305
|
+
break;
|
|
306
|
+
case "dispatch:issue:dispatched":
|
|
307
|
+
ctx.stopSpinner(`#${event.number} → session ${event.sessionId}`);
|
|
308
|
+
ctx.info(` ${sessionUrl(event.sessionId)}`);
|
|
309
|
+
break;
|
|
310
|
+
case "dispatch:issue:skipped":
|
|
311
|
+
ctx.warn(` ⊘ #${event.number}: ${event.reason}`);
|
|
312
|
+
break;
|
|
313
|
+
case "dispatch:done":
|
|
314
|
+
ctx.success(`Dispatch complete — ${event.dispatched} dispatched, ${event.skipped} skipped`);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/shared/ui/render/merge.ts
|
|
320
|
+
function renderMergeEvent(event, ctx) {
|
|
321
|
+
switch (event.type) {
|
|
322
|
+
case "merge:start":
|
|
323
|
+
ctx.info(`Merging ${event.prCount} PR(s) in ${event.owner}/${event.repo} [${event.mode}]`);
|
|
324
|
+
break;
|
|
325
|
+
case "merge:no-prs":
|
|
326
|
+
ctx.info("No PRs ready to merge.");
|
|
327
|
+
break;
|
|
328
|
+
case "merge:pr:processing":
|
|
329
|
+
ctx.startSpinner(`PR #${event.number}: ${event.title}${event.retry ? ` (retry ${event.retry})` : ""}`);
|
|
330
|
+
break;
|
|
331
|
+
case "merge:branch:updating":
|
|
332
|
+
ctx.startSpinner(`Updating branch for PR #${event.prNumber}…`);
|
|
333
|
+
break;
|
|
334
|
+
case "merge:branch:updated":
|
|
335
|
+
ctx.stopSpinner(`Branch updated for PR #${event.prNumber}`);
|
|
336
|
+
break;
|
|
337
|
+
case "merge:ci:waiting":
|
|
338
|
+
ctx.startSpinner(`Waiting for CI on PR #${event.prNumber}…`);
|
|
339
|
+
break;
|
|
340
|
+
case "merge:ci:check": {
|
|
341
|
+
const icon = event.status === "pass" ? "✓" : event.status === "fail" ? "✗" : "…";
|
|
342
|
+
const dur = event.duration ? ` (${event.duration}s)` : "";
|
|
343
|
+
ctx.info(` ${icon} ${event.name}${dur}`);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case "merge:ci:passed":
|
|
347
|
+
ctx.stopSpinner(`CI passed for PR #${event.prNumber}`);
|
|
348
|
+
break;
|
|
349
|
+
case "merge:ci:failed":
|
|
350
|
+
ctx.stopSpinner(`CI failed for PR #${event.prNumber}`);
|
|
351
|
+
break;
|
|
352
|
+
case "merge:ci:timeout":
|
|
353
|
+
ctx.stopSpinner(`CI timed out for PR #${event.prNumber}`);
|
|
354
|
+
break;
|
|
355
|
+
case "merge:ci:none":
|
|
356
|
+
ctx.stopSpinner(`No CI checks for PR #${event.prNumber}`);
|
|
357
|
+
break;
|
|
358
|
+
case "merge:pr:merging":
|
|
359
|
+
ctx.startSpinner(`Merging PR #${event.prNumber}…`);
|
|
360
|
+
break;
|
|
361
|
+
case "merge:pr:merged":
|
|
362
|
+
ctx.stopSpinner(`PR #${event.prNumber} merged ✓`);
|
|
363
|
+
break;
|
|
364
|
+
case "merge:pr:skipped":
|
|
365
|
+
ctx.warn(` ⊘ PR #${event.prNumber}: ${event.reason}`);
|
|
366
|
+
break;
|
|
367
|
+
case "merge:conflict:detected":
|
|
368
|
+
ctx.stopSpinner(`Conflict detected on PR #${event.prNumber}`);
|
|
369
|
+
break;
|
|
370
|
+
case "merge:redispatch:start":
|
|
371
|
+
ctx.startSpinner(`Re-dispatching PR #${event.oldPr}…`);
|
|
372
|
+
break;
|
|
373
|
+
case "merge:redispatch:waiting":
|
|
374
|
+
ctx.startSpinner(`Waiting for re-dispatched PR (was #${event.oldPr})…`);
|
|
375
|
+
break;
|
|
376
|
+
case "merge:redispatch:done":
|
|
377
|
+
ctx.stopSpinner(`Re-dispatched: #${event.oldPr} → #${event.newPr}`);
|
|
378
|
+
break;
|
|
379
|
+
case "merge:done":
|
|
380
|
+
ctx.success(`Merge complete — ${event.merged.length} merged, ${event.skipped.length} skipped`);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/shared/ui/render/error.ts
|
|
386
|
+
function renderErrorEvent(event, ctx) {
|
|
387
|
+
ctx.stopSpinner();
|
|
388
|
+
ctx.error(`[${event.code}] ${event.message}`);
|
|
389
|
+
if (event.suggestion)
|
|
390
|
+
ctx.info(` \uD83D\uDCA1 ${event.suggestion}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/shared/ui/interactive.ts
|
|
394
|
+
class InteractiveRenderer {
|
|
395
|
+
spinner = null;
|
|
396
|
+
ctx = {
|
|
397
|
+
info: (msg) => p.log.info(msg),
|
|
398
|
+
success: (msg) => p.log.success(msg),
|
|
399
|
+
warn: (msg) => p.log.warn(msg),
|
|
400
|
+
error: (msg) => p.log.error(msg),
|
|
401
|
+
message: (msg) => p.log.message(msg),
|
|
402
|
+
step: (msg) => p.log.step(msg),
|
|
403
|
+
startSpinner: (msg) => this.startSpinner(msg),
|
|
404
|
+
stopSpinner: (msg) => this.stopSpinner(msg)
|
|
405
|
+
};
|
|
406
|
+
start(title) {
|
|
407
|
+
p.intro(title);
|
|
408
|
+
}
|
|
409
|
+
end(message) {
|
|
410
|
+
this.stopSpinner();
|
|
411
|
+
p.outro(message);
|
|
412
|
+
}
|
|
413
|
+
error(message) {
|
|
414
|
+
this.stopSpinner();
|
|
415
|
+
p.log.error(message);
|
|
416
|
+
}
|
|
417
|
+
render(event) {
|
|
418
|
+
if (event.type.startsWith("init:"))
|
|
419
|
+
return renderInitEvent(event, this.ctx);
|
|
420
|
+
if (event.type.startsWith("configure:"))
|
|
421
|
+
return renderConfigureEvent(event, this.ctx);
|
|
422
|
+
if (event.type.startsWith("analyze:"))
|
|
423
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
424
|
+
if (event.type.startsWith("dispatch:"))
|
|
425
|
+
return renderDispatchEvent(event, this.ctx);
|
|
426
|
+
if (event.type.startsWith("merge:"))
|
|
427
|
+
return renderMergeEvent(event, this.ctx);
|
|
428
|
+
if (event.type === "error")
|
|
429
|
+
return renderErrorEvent(event, this.ctx);
|
|
430
|
+
}
|
|
431
|
+
startSpinner(message) {
|
|
432
|
+
this.stopSpinner();
|
|
433
|
+
this.spinner = p.spinner();
|
|
434
|
+
this.spinner.start(message);
|
|
435
|
+
}
|
|
436
|
+
stopSpinner(message) {
|
|
437
|
+
if (this.spinner) {
|
|
438
|
+
this.spinner.stop(message);
|
|
439
|
+
this.spinner = null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/shared/ui/plain.ts
|
|
445
|
+
class PlainRenderer {
|
|
446
|
+
ctx = {
|
|
447
|
+
info: (msg) => console.log(msg),
|
|
448
|
+
success: (msg) => console.log(msg),
|
|
449
|
+
warn: (msg) => console.log(msg),
|
|
450
|
+
error: (msg) => console.error(msg),
|
|
451
|
+
message: (msg) => console.log(msg),
|
|
452
|
+
step: (msg) => console.log(msg),
|
|
453
|
+
startSpinner: (msg) => console.log(msg),
|
|
454
|
+
stopSpinner: (msg) => {
|
|
455
|
+
if (msg)
|
|
456
|
+
console.log(` ✓ ${msg}`);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
start(title) {
|
|
460
|
+
console.log(`
|
|
461
|
+
═══ ${title} ═══
|
|
462
|
+
`);
|
|
463
|
+
}
|
|
464
|
+
end(message) {
|
|
465
|
+
console.log(`
|
|
466
|
+
═══ ${message} ═══
|
|
467
|
+
`);
|
|
468
|
+
}
|
|
469
|
+
error(message) {
|
|
470
|
+
console.error(`ERROR: ${message}`);
|
|
471
|
+
}
|
|
472
|
+
render(event) {
|
|
473
|
+
if (event.type.startsWith("init:"))
|
|
474
|
+
return renderInitEvent(event, this.ctx);
|
|
475
|
+
if (event.type.startsWith("configure:"))
|
|
476
|
+
return renderConfigureEvent(event, this.ctx);
|
|
477
|
+
if (event.type.startsWith("analyze:"))
|
|
478
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
479
|
+
if (event.type.startsWith("dispatch:"))
|
|
480
|
+
return renderDispatchEvent(event, this.ctx);
|
|
481
|
+
if (event.type.startsWith("merge:"))
|
|
482
|
+
return renderMergeEvent(event, this.ctx);
|
|
483
|
+
if (event.type === "error")
|
|
484
|
+
return renderErrorEvent(event, this.ctx);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/shared/ui/index.ts
|
|
489
|
+
function isInteractive() {
|
|
490
|
+
if (process.env.CI === "true")
|
|
491
|
+
return false;
|
|
492
|
+
if (!process.stdout.isTTY)
|
|
493
|
+
return false;
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
function createRenderer(interactive) {
|
|
497
|
+
const useInteractive = interactive ?? isInteractive();
|
|
498
|
+
return useInteractive ? new InteractiveRenderer : new PlainRenderer;
|
|
499
|
+
}
|
|
500
|
+
function createEmitter(renderer) {
|
|
501
|
+
return (event) => renderer.render(event);
|
|
502
|
+
}
|
|
503
|
+
// src/merge/spec.ts
|
|
504
|
+
import { z as z6 } from "zod";
|
|
505
|
+
var MergeMode = z6.enum(["label", "fleet-run"]);
|
|
506
|
+
var MergeInputSchema = z6.object({
|
|
507
|
+
mode: MergeMode.default("label"),
|
|
508
|
+
runId: z6.string().optional(),
|
|
509
|
+
baseBranch: z6.string().default("main"),
|
|
510
|
+
admin: z6.boolean().default(false),
|
|
511
|
+
maxCIWaitSeconds: z6.number().positive().default(600),
|
|
512
|
+
maxRetries: z6.number().nonnegative().default(2),
|
|
513
|
+
reDispatch: z6.boolean().default(false),
|
|
514
|
+
pollTimeoutSeconds: z6.number().positive().default(900),
|
|
515
|
+
owner: z6.string().min(1),
|
|
516
|
+
repo: z6.string().min(1)
|
|
517
|
+
}).refine((d) => d.mode !== "fleet-run" || !!d.runId, {
|
|
518
|
+
message: "--run-id is required when mode is fleet-run",
|
|
519
|
+
path: ["runId"]
|
|
520
|
+
});
|
|
521
|
+
var MergeErrorCode = z6.enum([
|
|
522
|
+
"NO_PRS_FOUND",
|
|
523
|
+
"CI_FAILED",
|
|
524
|
+
"CI_TIMEOUT",
|
|
525
|
+
"MERGE_FAILED",
|
|
526
|
+
"CONFLICT_RETRIES_EXHAUSTED",
|
|
527
|
+
"REDISPATCH_TIMEOUT",
|
|
528
|
+
"GITHUB_API_ERROR",
|
|
529
|
+
"UNKNOWN_ERROR"
|
|
530
|
+
]);
|
|
531
|
+
// src/merge/select/by-label.ts
|
|
532
|
+
async function selectByLabel(octokit, owner, repo, baseBranch) {
|
|
533
|
+
const { data: pulls } = await octokit.rest.pulls.list({
|
|
534
|
+
owner,
|
|
535
|
+
repo,
|
|
536
|
+
state: "open",
|
|
537
|
+
base: baseBranch,
|
|
538
|
+
per_page: 100
|
|
539
|
+
});
|
|
540
|
+
const labeledPRs = pulls.filter((pr) => pr.labels.some((label) => label.name === "fleet-merge-ready")).sort((a, b) => a.number - b.number).map((pr) => ({
|
|
541
|
+
number: pr.number,
|
|
542
|
+
headRef: pr.head.ref,
|
|
543
|
+
headSha: pr.head.sha,
|
|
544
|
+
body: pr.body
|
|
545
|
+
}));
|
|
546
|
+
return labeledPRs;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/merge/select/by-fleet-run.ts
|
|
550
|
+
var FLEET_RUN_MARKER_PREFIX = "<!-- fleet-run:";
|
|
551
|
+
async function selectByFleetRun(octokit, owner, repo, baseBranch, runId) {
|
|
552
|
+
const { data: pulls } = await octokit.rest.pulls.list({
|
|
553
|
+
owner,
|
|
554
|
+
repo,
|
|
555
|
+
state: "open",
|
|
556
|
+
base: baseBranch,
|
|
557
|
+
per_page: 100
|
|
558
|
+
});
|
|
559
|
+
const marker = `${FLEET_RUN_MARKER_PREFIX} ${runId} -->`;
|
|
560
|
+
const matchingPRs = pulls.filter((pr) => pr.body?.includes(marker)).sort((a, b) => a.number - b.number).map((pr) => ({
|
|
561
|
+
number: pr.number,
|
|
562
|
+
headRef: pr.head.ref,
|
|
563
|
+
headSha: pr.head.sha,
|
|
564
|
+
body: pr.body
|
|
565
|
+
}));
|
|
566
|
+
return matchingPRs;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/merge/ops/update-branch.ts
|
|
570
|
+
async function updateBranch(octokit, owner, repo, prNumber, emit) {
|
|
571
|
+
try {
|
|
572
|
+
emit({ type: "merge:branch:updating", prNumber });
|
|
573
|
+
await octokit.rest.pulls.updateBranch({
|
|
574
|
+
owner,
|
|
575
|
+
repo,
|
|
576
|
+
pull_number: prNumber
|
|
577
|
+
});
|
|
578
|
+
emit({ type: "merge:branch:updated", prNumber });
|
|
579
|
+
return { ok: true, conflict: false };
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
582
|
+
if (status === 422) {
|
|
583
|
+
emit({ type: "merge:conflict:detected", prNumber });
|
|
584
|
+
return { ok: false, conflict: true };
|
|
585
|
+
}
|
|
586
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
587
|
+
return { ok: false, conflict: false, error: message };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/merge/ops/wait-for-ci.ts
|
|
592
|
+
async function waitForCI(octokit, owner, repo, prNumber, maxWaitMs, emit, sleep) {
|
|
593
|
+
const { data: prData } = await octokit.rest.pulls.get({
|
|
594
|
+
owner,
|
|
595
|
+
repo,
|
|
596
|
+
pull_number: prNumber
|
|
597
|
+
});
|
|
598
|
+
const headSha = prData.head.sha;
|
|
599
|
+
emit({ type: "merge:ci:waiting", prNumber });
|
|
600
|
+
const start = Date.now();
|
|
601
|
+
while (Date.now() - start < maxWaitMs) {
|
|
602
|
+
const { data } = await octokit.rest.checks.listForRef({
|
|
603
|
+
owner,
|
|
604
|
+
repo,
|
|
605
|
+
ref: headSha
|
|
606
|
+
});
|
|
607
|
+
if (data.check_runs.length === 0) {
|
|
608
|
+
emit({ type: "merge:ci:none", prNumber });
|
|
609
|
+
return "none";
|
|
610
|
+
}
|
|
611
|
+
for (const run of data.check_runs) {
|
|
612
|
+
if (run.status === "completed") {
|
|
613
|
+
const passed = run.conclusion === "success" || run.conclusion === "skipped";
|
|
614
|
+
const durationMs = run.started_at && run.completed_at ? new Date(run.completed_at).getTime() - new Date(run.started_at).getTime() : undefined;
|
|
615
|
+
emit({
|
|
616
|
+
type: "merge:ci:check",
|
|
617
|
+
name: run.name,
|
|
618
|
+
status: passed ? "pass" : "fail",
|
|
619
|
+
duration: durationMs ? Math.round(durationMs / 1000) : undefined
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const allComplete = data.check_runs.every((run) => run.status === "completed");
|
|
624
|
+
const allPassed = data.check_runs.every((run) => run.conclusion === "success" || run.conclusion === "skipped");
|
|
625
|
+
if (allComplete && allPassed) {
|
|
626
|
+
emit({ type: "merge:ci:passed", prNumber });
|
|
627
|
+
return "pass";
|
|
628
|
+
}
|
|
629
|
+
if (allComplete && !allPassed) {
|
|
630
|
+
emit({ type: "merge:ci:failed", prNumber });
|
|
631
|
+
return "fail";
|
|
632
|
+
}
|
|
633
|
+
await sleep(30000);
|
|
634
|
+
}
|
|
635
|
+
emit({ type: "merge:ci:timeout", prNumber });
|
|
636
|
+
return "timeout";
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/merge/ops/squash-merge.ts
|
|
640
|
+
async function squashMerge(octokit, owner, repo, prNumber) {
|
|
641
|
+
try {
|
|
642
|
+
await octokit.rest.pulls.merge({
|
|
643
|
+
owner,
|
|
644
|
+
repo,
|
|
645
|
+
pull_number: prNumber,
|
|
646
|
+
merge_method: "squash"
|
|
647
|
+
});
|
|
648
|
+
return { ok: true };
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
651
|
+
return { ok: false, error: message };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/merge/ops/redispatch.ts
|
|
656
|
+
async function redispatch(octokit, owner, repo, oldPr, baseBranch, pollTimeoutSeconds, emit, sleep) {
|
|
657
|
+
emit({ type: "merge:redispatch:start", oldPr: oldPr.number });
|
|
658
|
+
try {
|
|
659
|
+
await octokit.rest.pulls.update({
|
|
660
|
+
owner,
|
|
661
|
+
repo,
|
|
662
|
+
pull_number: oldPr.number,
|
|
663
|
+
state: "closed",
|
|
664
|
+
body: `${oldPr.body ?? ""}
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
⚠️ Closed by fleet-merge: merge conflict detected. Task re-dispatched.`
|
|
668
|
+
});
|
|
669
|
+
} catch {}
|
|
670
|
+
try {
|
|
671
|
+
const { jules } = await import("@google/jules-sdk");
|
|
672
|
+
const session = await jules.session({
|
|
673
|
+
prompt: oldPr.body ?? "",
|
|
674
|
+
source: {
|
|
675
|
+
github: `${owner}/${repo}`,
|
|
676
|
+
baseBranch
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
emit({ type: "merge:redispatch:waiting", oldPr: oldPr.number });
|
|
680
|
+
const start = Date.now();
|
|
681
|
+
const timeoutMs = pollTimeoutSeconds * 1000;
|
|
682
|
+
const pollIntervalMs = 30000;
|
|
683
|
+
while (Date.now() - start < timeoutMs) {
|
|
684
|
+
await sleep(pollIntervalMs);
|
|
685
|
+
const { data: pulls } = await octokit.rest.pulls.list({
|
|
686
|
+
owner,
|
|
687
|
+
repo,
|
|
688
|
+
state: "open",
|
|
689
|
+
base: baseBranch,
|
|
690
|
+
per_page: 100
|
|
691
|
+
});
|
|
692
|
+
const newPr = pulls.find((pr) => pr.head.ref.includes(session.id) || pr.body?.includes(session.id));
|
|
693
|
+
if (newPr) {
|
|
694
|
+
const result = {
|
|
695
|
+
number: newPr.number,
|
|
696
|
+
headRef: newPr.head.ref,
|
|
697
|
+
headSha: newPr.head.sha,
|
|
698
|
+
body: newPr.body
|
|
699
|
+
};
|
|
700
|
+
emit({
|
|
701
|
+
type: "merge:redispatch:done",
|
|
702
|
+
oldPr: oldPr.number,
|
|
703
|
+
newPr: newPr.number
|
|
704
|
+
});
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch (error) {
|
|
709
|
+
emit({
|
|
710
|
+
type: "error",
|
|
711
|
+
code: "REDISPATCH_FAILED",
|
|
712
|
+
message: `Re-dispatch failed: ${error instanceof Error ? error.message : error}`
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/merge/handler.ts
|
|
719
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
720
|
+
|
|
721
|
+
class MergeHandler {
|
|
722
|
+
octokit;
|
|
723
|
+
emit;
|
|
724
|
+
sleep;
|
|
725
|
+
constructor(deps) {
|
|
726
|
+
this.octokit = deps.octokit;
|
|
727
|
+
this.emit = deps.emit ?? (() => {});
|
|
728
|
+
this.sleep = deps.sleep ?? defaultSleep;
|
|
729
|
+
}
|
|
730
|
+
async execute(input) {
|
|
731
|
+
try {
|
|
732
|
+
const prs = await this.selectPRs(input);
|
|
733
|
+
if (prs.length === 0) {
|
|
734
|
+
this.emit({ type: "merge:no-prs" });
|
|
735
|
+
return ok({ merged: [], skipped: [], redispatched: [] });
|
|
736
|
+
}
|
|
737
|
+
this.emit({
|
|
738
|
+
type: "merge:start",
|
|
739
|
+
owner: input.owner,
|
|
740
|
+
repo: input.repo,
|
|
741
|
+
mode: input.mode,
|
|
742
|
+
prCount: prs.length
|
|
743
|
+
});
|
|
744
|
+
const merged = [];
|
|
745
|
+
const skipped = [];
|
|
746
|
+
const redispatched = [];
|
|
747
|
+
for (const pr of prs) {
|
|
748
|
+
let currentPr = pr;
|
|
749
|
+
let retryCount = 0;
|
|
750
|
+
let prMerged = false;
|
|
751
|
+
while (!prMerged) {
|
|
752
|
+
this.emit({
|
|
753
|
+
type: "merge:pr:processing",
|
|
754
|
+
number: currentPr.number,
|
|
755
|
+
title: currentPr.body?.split(`
|
|
756
|
+
`)[0] ?? `PR #${currentPr.number}`,
|
|
757
|
+
retry: retryCount > 0 ? retryCount : undefined
|
|
758
|
+
});
|
|
759
|
+
if (prs.indexOf(pr) > 0 || retryCount > 0) {
|
|
760
|
+
const updateResult = await updateBranch(this.octokit, input.owner, input.repo, currentPr.number, this.emit);
|
|
761
|
+
if (!updateResult.ok && updateResult.conflict) {
|
|
762
|
+
if (!input.reDispatch) {
|
|
763
|
+
return fail("CONFLICT_RETRIES_EXHAUSTED", `Merge conflict detected for PR #${currentPr.number}. Use --re-dispatch to automatically retry.`, false, `Review PR: https://github.com/${input.owner}/${input.repo}/pull/${currentPr.number}`);
|
|
764
|
+
}
|
|
765
|
+
if (retryCount >= input.maxRetries) {
|
|
766
|
+
return fail("CONFLICT_RETRIES_EXHAUSTED", `Conflict persists for PR #${currentPr.number} after ${input.maxRetries} retries. Human intervention required.`, false, `Review PR: https://github.com/${input.owner}/${input.repo}/pull/${currentPr.number}`);
|
|
767
|
+
}
|
|
768
|
+
const newPr = await redispatch(this.octokit, input.owner, input.repo, currentPr, input.baseBranch, input.pollTimeoutSeconds, this.emit, this.sleep);
|
|
769
|
+
if (!newPr) {
|
|
770
|
+
return fail("REDISPATCH_TIMEOUT", `Timed out waiting for re-dispatched PR for #${currentPr.number}.`, true);
|
|
771
|
+
}
|
|
772
|
+
redispatched.push({
|
|
773
|
+
oldPr: currentPr.number,
|
|
774
|
+
newPr: newPr.number
|
|
775
|
+
});
|
|
776
|
+
currentPr = newPr;
|
|
777
|
+
retryCount++;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (!updateResult.ok && !updateResult.conflict) {
|
|
781
|
+
return fail("GITHUB_API_ERROR", `Failed to update branch for PR #${currentPr.number}: ${updateResult.error}`, true);
|
|
782
|
+
}
|
|
783
|
+
await this.sleep(5000);
|
|
784
|
+
}
|
|
785
|
+
const ciResult = await waitForCI(this.octokit, input.owner, input.repo, currentPr.number, input.maxCIWaitSeconds * 1000, this.emit, this.sleep);
|
|
786
|
+
if (ciResult === "none") {} else if (ciResult === "fail") {
|
|
787
|
+
this.emit({
|
|
788
|
+
type: "merge:pr:skipped",
|
|
789
|
+
prNumber: currentPr.number,
|
|
790
|
+
reason: "CI failure"
|
|
791
|
+
});
|
|
792
|
+
skipped.push(currentPr.number);
|
|
793
|
+
break;
|
|
794
|
+
} else if (ciResult === "timeout") {
|
|
795
|
+
this.emit({
|
|
796
|
+
type: "merge:pr:skipped",
|
|
797
|
+
prNumber: currentPr.number,
|
|
798
|
+
reason: "CI timeout"
|
|
799
|
+
});
|
|
800
|
+
skipped.push(currentPr.number);
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
this.emit({ type: "merge:pr:merging", prNumber: currentPr.number });
|
|
804
|
+
const mergeResult = await squashMerge(this.octokit, input.owner, input.repo, currentPr.number);
|
|
805
|
+
if (!mergeResult.ok) {
|
|
806
|
+
return fail("MERGE_FAILED", `Failed to merge PR #${currentPr.number}: ${mergeResult.error}`, false);
|
|
807
|
+
}
|
|
808
|
+
this.emit({ type: "merge:pr:merged", prNumber: currentPr.number });
|
|
809
|
+
merged.push(currentPr.number);
|
|
810
|
+
prMerged = true;
|
|
811
|
+
}
|
|
812
|
+
await this.sleep(5000);
|
|
813
|
+
}
|
|
814
|
+
this.emit({ type: "merge:done", merged, skipped, redispatched });
|
|
815
|
+
return ok({ merged, skipped, redispatched });
|
|
816
|
+
} catch (error) {
|
|
817
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async selectPRs(input) {
|
|
821
|
+
if (input.mode === "fleet-run") {
|
|
822
|
+
return selectByFleetRun(this.octokit, input.owner, input.repo, input.baseBranch, input.runId);
|
|
823
|
+
}
|
|
824
|
+
return selectByLabel(this.octokit, input.owner, input.repo, input.baseBranch);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// src/init/spec.ts
|
|
828
|
+
import { z as z7 } from "zod";
|
|
829
|
+
var InitInputSchema = z7.object({
|
|
830
|
+
repo: z7.string().regex(/^[^/]+\/[^/]+$/, "Must be in owner/repo format").optional(),
|
|
831
|
+
owner: z7.string().min(1),
|
|
832
|
+
repoName: z7.string().min(1),
|
|
833
|
+
baseBranch: z7.string().default("main")
|
|
834
|
+
});
|
|
835
|
+
var InitErrorCode = z7.enum([
|
|
836
|
+
"REPO_NOT_FOUND",
|
|
837
|
+
"BRANCH_CREATE_FAILED",
|
|
838
|
+
"FILE_COMMIT_FAILED",
|
|
839
|
+
"PR_CREATE_FAILED",
|
|
840
|
+
"LABEL_CREATE_FAILED",
|
|
841
|
+
"GITHUB_API_ERROR",
|
|
842
|
+
"UNKNOWN_ERROR"
|
|
843
|
+
]);
|
|
844
|
+
// src/init/templates/analyze.ts
|
|
845
|
+
var FLEET_ANALYZE_TEMPLATE = {
|
|
846
|
+
repoPath: ".github/workflows/fleet-analyze.yml",
|
|
847
|
+
content: `# Generated by @google/jules-fleet init
|
|
848
|
+
# https://github.com/google-labs-code/jules-sdk
|
|
849
|
+
|
|
850
|
+
name: Fleet Analyze
|
|
851
|
+
|
|
852
|
+
on:
|
|
853
|
+
schedule:
|
|
854
|
+
- cron: '0 5 * * *' # Daily at 5am UTC — edit to your preference
|
|
855
|
+
workflow_dispatch:
|
|
856
|
+
inputs:
|
|
857
|
+
goal:
|
|
858
|
+
description: 'Path to goal file (or blank for all)'
|
|
859
|
+
type: string
|
|
860
|
+
default: ''
|
|
861
|
+
milestone:
|
|
862
|
+
description: 'Milestone ID override'
|
|
863
|
+
type: string
|
|
864
|
+
default: ''
|
|
865
|
+
|
|
866
|
+
concurrency:
|
|
867
|
+
group: fleet-analyze
|
|
868
|
+
cancel-in-progress: false
|
|
869
|
+
|
|
870
|
+
jobs:
|
|
871
|
+
analyze:
|
|
872
|
+
runs-on: ubuntu-latest
|
|
873
|
+
permissions:
|
|
874
|
+
contents: read
|
|
875
|
+
issues: write
|
|
876
|
+
steps:
|
|
877
|
+
- uses: actions/checkout@v4
|
|
878
|
+
- uses: actions/setup-node@v4
|
|
879
|
+
with:
|
|
880
|
+
node-version: '22'
|
|
881
|
+
- run: npx @google/jules-fleet analyze --goal "\${{ inputs.goal }}" --milestone "\${{ inputs.milestone }}"
|
|
882
|
+
env:
|
|
883
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
884
|
+
JULES_API_KEY: \${{ secrets.JULES_API_KEY }}
|
|
885
|
+
`
|
|
886
|
+
};
|
|
887
|
+
// src/init/templates/dispatch.ts
|
|
888
|
+
var FLEET_DISPATCH_TEMPLATE = {
|
|
889
|
+
repoPath: ".github/workflows/fleet-dispatch.yml",
|
|
890
|
+
content: `# Generated by @google/jules-fleet init
|
|
891
|
+
# https://github.com/google-labs-code/jules-sdk
|
|
892
|
+
|
|
893
|
+
name: Fleet Dispatch
|
|
894
|
+
|
|
895
|
+
on:
|
|
896
|
+
schedule:
|
|
897
|
+
- cron: '0 6 * * *' # Daily at 6am UTC — edit to your preference
|
|
898
|
+
workflow_dispatch:
|
|
899
|
+
inputs:
|
|
900
|
+
milestone:
|
|
901
|
+
description: 'Milestone ID to dispatch'
|
|
902
|
+
type: string
|
|
903
|
+
required: true
|
|
904
|
+
|
|
905
|
+
concurrency:
|
|
906
|
+
group: fleet-dispatch
|
|
907
|
+
cancel-in-progress: false
|
|
908
|
+
|
|
909
|
+
jobs:
|
|
910
|
+
dispatch:
|
|
911
|
+
runs-on: ubuntu-latest
|
|
912
|
+
permissions:
|
|
913
|
+
contents: read
|
|
914
|
+
issues: write
|
|
915
|
+
steps:
|
|
916
|
+
- uses: actions/checkout@v4
|
|
917
|
+
- uses: actions/setup-node@v4
|
|
918
|
+
with:
|
|
919
|
+
node-version: '22'
|
|
920
|
+
- run: npx @google/jules-fleet dispatch --milestone \${{ inputs.milestone }}
|
|
921
|
+
env:
|
|
922
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
923
|
+
JULES_API_KEY: \${{ secrets.JULES_API_KEY }}
|
|
924
|
+
`
|
|
925
|
+
};
|
|
926
|
+
// src/init/templates/merge.ts
|
|
927
|
+
var FLEET_MERGE_TEMPLATE = {
|
|
928
|
+
repoPath: ".github/workflows/fleet-merge.yml",
|
|
929
|
+
content: `# Generated by @google/jules-fleet init
|
|
930
|
+
# https://github.com/google-labs-code/jules-sdk
|
|
931
|
+
|
|
932
|
+
name: Fleet Merge
|
|
933
|
+
|
|
934
|
+
on:
|
|
935
|
+
schedule:
|
|
936
|
+
- cron: '0 */4 * * *' # Every 4 hours — edit to your preference
|
|
937
|
+
workflow_dispatch:
|
|
938
|
+
inputs:
|
|
939
|
+
mode:
|
|
940
|
+
description: 'PR selection mode'
|
|
941
|
+
type: choice
|
|
942
|
+
options:
|
|
943
|
+
- label
|
|
944
|
+
- fleet-run
|
|
945
|
+
default: 'label'
|
|
946
|
+
fleet_run_id:
|
|
947
|
+
description: 'Fleet run ID (required for fleet-run mode)'
|
|
948
|
+
type: string
|
|
949
|
+
default: ''
|
|
950
|
+
re_dispatch:
|
|
951
|
+
description: 'Auto re-dispatch on merge conflict'
|
|
952
|
+
type: boolean
|
|
953
|
+
default: true
|
|
954
|
+
|
|
955
|
+
concurrency:
|
|
956
|
+
group: fleet-merge
|
|
957
|
+
cancel-in-progress: false
|
|
958
|
+
|
|
959
|
+
jobs:
|
|
960
|
+
merge:
|
|
961
|
+
runs-on: ubuntu-latest
|
|
962
|
+
permissions:
|
|
963
|
+
contents: write
|
|
964
|
+
pull-requests: write
|
|
965
|
+
issues: write
|
|
966
|
+
steps:
|
|
967
|
+
- uses: actions/checkout@v4
|
|
968
|
+
- uses: actions/setup-node@v4
|
|
969
|
+
with:
|
|
970
|
+
node-version: '22'
|
|
971
|
+
- run: |
|
|
972
|
+
REDISPATCH_FLAG=""
|
|
973
|
+
if [ "\${{ inputs.re_dispatch }}" = "true" ]; then
|
|
974
|
+
REDISPATCH_FLAG="--re-dispatch"
|
|
975
|
+
fi
|
|
976
|
+
npx @google/jules-fleet merge \\
|
|
977
|
+
--mode \${{ inputs.mode || 'label' }} \\
|
|
978
|
+
--run-id "\${{ inputs.fleet_run_id }}" \\
|
|
979
|
+
$REDISPATCH_FLAG
|
|
980
|
+
env:
|
|
981
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
982
|
+
JULES_API_KEY: \${{ secrets.JULES_API_KEY }}
|
|
983
|
+
`
|
|
984
|
+
};
|
|
985
|
+
// src/init/templates.ts
|
|
986
|
+
var WORKFLOW_TEMPLATES = [
|
|
987
|
+
FLEET_ANALYZE_TEMPLATE,
|
|
988
|
+
FLEET_DISPATCH_TEMPLATE,
|
|
989
|
+
FLEET_MERGE_TEMPLATE
|
|
990
|
+
];
|
|
991
|
+
|
|
992
|
+
// src/init/templates/example-goal.ts
|
|
993
|
+
var EXAMPLE_GOAL = `---
|
|
994
|
+
milestone: "1"
|
|
995
|
+
---
|
|
996
|
+
|
|
997
|
+
# Example Fleet Goal
|
|
998
|
+
|
|
999
|
+
Analyze the codebase for potential improvements and create
|
|
1000
|
+
issues for the engineering team.
|
|
1001
|
+
|
|
1002
|
+
## Tools
|
|
1003
|
+
- Test Coverage: \`npx vitest --coverage --json\`
|
|
1004
|
+
|
|
1005
|
+
## Assessment Hints
|
|
1006
|
+
- Focus on missing error handling in API routes
|
|
1007
|
+
- Look for hardcoded configuration values
|
|
1008
|
+
|
|
1009
|
+
## Insight Hints
|
|
1010
|
+
- Report on overall test coverage metrics
|
|
1011
|
+
- Note any unusually complex functions (cyclomatic complexity)
|
|
1012
|
+
|
|
1013
|
+
## Constraints
|
|
1014
|
+
- Do NOT propose changes already covered by open issues
|
|
1015
|
+
- Do NOT propose changes rejected in recently closed issues
|
|
1016
|
+
- Keep tasks small and isolated — one logical change per issue
|
|
1017
|
+
`;
|
|
1018
|
+
|
|
1019
|
+
// src/init/ops/create-branch.ts
|
|
1020
|
+
async function createBranch(octokit, owner, repo, baseBranch, emit) {
|
|
1021
|
+
const { data: refData } = await octokit.rest.git.getRef({
|
|
1022
|
+
owner,
|
|
1023
|
+
repo,
|
|
1024
|
+
ref: `heads/${baseBranch}`
|
|
1025
|
+
});
|
|
1026
|
+
const baseSha = refData.object.sha;
|
|
1027
|
+
const branchName = `fleet-init-${Date.now()}`;
|
|
1028
|
+
emit({ type: "init:branch:creating", name: branchName, base: baseBranch });
|
|
1029
|
+
try {
|
|
1030
|
+
await octokit.rest.git.createRef({
|
|
1031
|
+
owner,
|
|
1032
|
+
repo,
|
|
1033
|
+
ref: `refs/heads/${branchName}`,
|
|
1034
|
+
sha: baseSha
|
|
1035
|
+
});
|
|
1036
|
+
emit({ type: "init:branch:created", name: branchName });
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
return fail("BRANCH_CREATE_FAILED", `Failed to create branch "${branchName}": ${error instanceof Error ? error.message : error}`, true);
|
|
1039
|
+
}
|
|
1040
|
+
return { branchName, baseSha };
|
|
1041
|
+
}
|
|
1042
|
+
function isBranchResult(result) {
|
|
1043
|
+
return "success" in result;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/init/ops/commit-files.ts
|
|
1047
|
+
async function commitFiles(ctx, templates, exampleGoal) {
|
|
1048
|
+
const filesCreated = [];
|
|
1049
|
+
for (const tmpl of templates) {
|
|
1050
|
+
try {
|
|
1051
|
+
await ctx.octokit.rest.repos.createOrUpdateFileContents({
|
|
1052
|
+
owner: ctx.owner,
|
|
1053
|
+
repo: ctx.repo,
|
|
1054
|
+
path: tmpl.repoPath,
|
|
1055
|
+
message: `chore: add ${tmpl.repoPath}`,
|
|
1056
|
+
content: Buffer.from(tmpl.content).toString("base64"),
|
|
1057
|
+
branch: ctx.branchName
|
|
1058
|
+
});
|
|
1059
|
+
filesCreated.push(tmpl.repoPath);
|
|
1060
|
+
ctx.emit({ type: "init:file:committed", path: tmpl.repoPath });
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
1063
|
+
if (status === 422) {
|
|
1064
|
+
ctx.emit({ type: "init:file:skipped", path: tmpl.repoPath, reason: "already exists" });
|
|
1065
|
+
} else {
|
|
1066
|
+
return fail("FILE_COMMIT_FAILED", `Failed to commit "${tmpl.repoPath}": ${error instanceof Error ? error.message : error}`, true);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
await ctx.octokit.rest.repos.createOrUpdateFileContents({
|
|
1072
|
+
owner: ctx.owner,
|
|
1073
|
+
repo: ctx.repo,
|
|
1074
|
+
path: ".fleet/goals/example.md",
|
|
1075
|
+
message: "chore: add example fleet goal",
|
|
1076
|
+
content: Buffer.from(exampleGoal).toString("base64"),
|
|
1077
|
+
branch: ctx.branchName
|
|
1078
|
+
});
|
|
1079
|
+
filesCreated.push(".fleet/goals/example.md");
|
|
1080
|
+
ctx.emit({ type: "init:file:committed", path: ".fleet/goals/example.md" });
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
1083
|
+
if (status !== 422) {
|
|
1084
|
+
ctx.emit({
|
|
1085
|
+
type: "init:file:skipped",
|
|
1086
|
+
path: ".fleet/goals/example.md",
|
|
1087
|
+
reason: `${error instanceof Error ? error.message : error}`
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return filesCreated;
|
|
1092
|
+
}
|
|
1093
|
+
function isCommitResult(result) {
|
|
1094
|
+
return !Array.isArray(result);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/init/ops/pr-body.ts
|
|
1098
|
+
function buildInitPRBody(filesCreated) {
|
|
1099
|
+
return [
|
|
1100
|
+
"## Fleet Initialization",
|
|
1101
|
+
"",
|
|
1102
|
+
"This PR adds the fleet workflow files for automated issue dispatch, merge, and analysis.",
|
|
1103
|
+
"",
|
|
1104
|
+
"### Files added",
|
|
1105
|
+
...filesCreated.map((f) => `- \`${f}\``),
|
|
1106
|
+
"",
|
|
1107
|
+
"### Next steps",
|
|
1108
|
+
"1. Merge this PR",
|
|
1109
|
+
"2. Add `JULES_API_KEY` to your repo secrets",
|
|
1110
|
+
"3. Create milestones and issues with the `fleet` label",
|
|
1111
|
+
"4. Run `jules-fleet configure labels` to set up labels (or they were already created)"
|
|
1112
|
+
].join(`
|
|
1113
|
+
`);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/init/ops/create-pr.ts
|
|
1117
|
+
async function createInitPR(ctx, baseBranch, filesCreated) {
|
|
1118
|
+
ctx.emit({ type: "init:pr:creating" });
|
|
1119
|
+
try {
|
|
1120
|
+
const { data: pr } = await ctx.octokit.rest.pulls.create({
|
|
1121
|
+
owner: ctx.owner,
|
|
1122
|
+
repo: ctx.repo,
|
|
1123
|
+
title: "chore: initialize fleet workflows",
|
|
1124
|
+
body: buildInitPRBody(filesCreated),
|
|
1125
|
+
head: ctx.branchName,
|
|
1126
|
+
base: baseBranch
|
|
1127
|
+
});
|
|
1128
|
+
ctx.emit({ type: "init:pr:created", url: pr.html_url, number: pr.number });
|
|
1129
|
+
return { prUrl: pr.html_url, prNumber: pr.number };
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
return fail("PR_CREATE_FAILED", `Failed to create PR: ${error instanceof Error ? error.message : error}`, true);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function isPRResult(result) {
|
|
1135
|
+
return "success" in result;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/init/handler.ts
|
|
1139
|
+
class InitHandler {
|
|
1140
|
+
octokit;
|
|
1141
|
+
emit;
|
|
1142
|
+
labelConfigurator;
|
|
1143
|
+
constructor(deps) {
|
|
1144
|
+
this.octokit = deps.octokit;
|
|
1145
|
+
this.emit = deps.emit ?? (() => {});
|
|
1146
|
+
this.labelConfigurator = deps.labelConfigurator;
|
|
1147
|
+
}
|
|
1148
|
+
async execute(input) {
|
|
1149
|
+
try {
|
|
1150
|
+
const { owner, repoName: repo, baseBranch } = input;
|
|
1151
|
+
this.emit({ type: "init:start", owner, repo });
|
|
1152
|
+
const branchResult = await createBranch(this.octokit, owner, repo, baseBranch, this.emit);
|
|
1153
|
+
if (isBranchResult(branchResult))
|
|
1154
|
+
return branchResult;
|
|
1155
|
+
const { branchName } = branchResult;
|
|
1156
|
+
const ctx = {
|
|
1157
|
+
octokit: this.octokit,
|
|
1158
|
+
owner,
|
|
1159
|
+
repo,
|
|
1160
|
+
branchName,
|
|
1161
|
+
emit: this.emit
|
|
1162
|
+
};
|
|
1163
|
+
const filesResult = await commitFiles(ctx, WORKFLOW_TEMPLATES, EXAMPLE_GOAL);
|
|
1164
|
+
if (isCommitResult(filesResult))
|
|
1165
|
+
return filesResult;
|
|
1166
|
+
const filesCreated = filesResult;
|
|
1167
|
+
if (filesCreated.length === 0) {
|
|
1168
|
+
this.emit({
|
|
1169
|
+
type: "error",
|
|
1170
|
+
code: "ALREADY_INITIALIZED",
|
|
1171
|
+
message: "All fleet files already exist — nothing to commit.",
|
|
1172
|
+
suggestion: "This repo appears to be already initialized. Use jules-fleet configure to update settings."
|
|
1173
|
+
});
|
|
1174
|
+
return fail("FILE_COMMIT_FAILED", "All fleet files already exist — nothing to commit.", false, "This repo appears to be already initialized. Use jules-fleet configure to update settings.");
|
|
1175
|
+
}
|
|
1176
|
+
const prResult = await createInitPR(ctx, baseBranch, filesCreated);
|
|
1177
|
+
if (isPRResult(prResult))
|
|
1178
|
+
return prResult;
|
|
1179
|
+
const { prUrl, prNumber } = prResult;
|
|
1180
|
+
let labelsCreated = [];
|
|
1181
|
+
if (this.labelConfigurator) {
|
|
1182
|
+
const labelResult = await this.labelConfigurator.execute({
|
|
1183
|
+
resource: "labels",
|
|
1184
|
+
action: "create",
|
|
1185
|
+
owner,
|
|
1186
|
+
repo
|
|
1187
|
+
});
|
|
1188
|
+
labelsCreated = labelResult.success ? labelResult.data.created : [];
|
|
1189
|
+
}
|
|
1190
|
+
this.emit({
|
|
1191
|
+
type: "init:done",
|
|
1192
|
+
prUrl,
|
|
1193
|
+
files: filesCreated,
|
|
1194
|
+
labels: labelsCreated
|
|
1195
|
+
});
|
|
1196
|
+
return ok({ prUrl, prNumber, filesCreated, labelsCreated });
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
// src/configure/spec.ts
|
|
1203
|
+
import { z as z8 } from "zod";
|
|
1204
|
+
var ConfigureAction = z8.enum(["create", "delete"]);
|
|
1205
|
+
var ConfigureResource = z8.enum(["labels"]);
|
|
1206
|
+
var ConfigureInputSchema = z8.object({
|
|
1207
|
+
resource: ConfigureResource,
|
|
1208
|
+
action: ConfigureAction.default("create"),
|
|
1209
|
+
owner: z8.string().min(1),
|
|
1210
|
+
repo: z8.string().min(1)
|
|
1211
|
+
});
|
|
1212
|
+
var ConfigureErrorCode = z8.enum([
|
|
1213
|
+
"GITHUB_API_ERROR",
|
|
1214
|
+
"UNKNOWN_ERROR"
|
|
1215
|
+
]);
|
|
1216
|
+
// src/configure/labels.ts
|
|
1217
|
+
var FLEET_LABELS = [
|
|
1218
|
+
{
|
|
1219
|
+
name: "fleet-merge-ready",
|
|
1220
|
+
color: "0e8a16",
|
|
1221
|
+
description: "Ready for fleet sequential merge"
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: "fleet",
|
|
1225
|
+
color: "1d76db",
|
|
1226
|
+
description: "Fleet-managed issue"
|
|
1227
|
+
}
|
|
1228
|
+
];
|
|
1229
|
+
|
|
1230
|
+
// src/configure/handler.ts
|
|
1231
|
+
class ConfigureHandler {
|
|
1232
|
+
octokit;
|
|
1233
|
+
emit;
|
|
1234
|
+
constructor(deps) {
|
|
1235
|
+
this.octokit = deps.octokit;
|
|
1236
|
+
this.emit = deps.emit ?? (() => {});
|
|
1237
|
+
}
|
|
1238
|
+
async execute(input) {
|
|
1239
|
+
try {
|
|
1240
|
+
this.emit({
|
|
1241
|
+
type: "configure:start",
|
|
1242
|
+
resource: input.resource,
|
|
1243
|
+
owner: input.owner,
|
|
1244
|
+
repo: input.repo
|
|
1245
|
+
});
|
|
1246
|
+
if (input.resource === "labels") {
|
|
1247
|
+
const result = input.action === "create" ? await this.createLabels(input.owner, input.repo) : await this.deleteLabels(input.owner, input.repo);
|
|
1248
|
+
this.emit({ type: "configure:done" });
|
|
1249
|
+
return result;
|
|
1250
|
+
}
|
|
1251
|
+
return fail("UNKNOWN_ERROR", `Unknown resource: ${input.resource}`, false);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async createLabels(owner, repo) {
|
|
1257
|
+
const created = [];
|
|
1258
|
+
const skipped = [];
|
|
1259
|
+
for (const label of FLEET_LABELS) {
|
|
1260
|
+
try {
|
|
1261
|
+
await this.octokit.rest.issues.createLabel({
|
|
1262
|
+
owner,
|
|
1263
|
+
repo,
|
|
1264
|
+
name: label.name,
|
|
1265
|
+
color: label.color,
|
|
1266
|
+
description: label.description
|
|
1267
|
+
});
|
|
1268
|
+
created.push(label.name);
|
|
1269
|
+
this.emit({ type: "configure:label:created", name: label.name });
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
1272
|
+
if (status === 422) {
|
|
1273
|
+
skipped.push(label.name);
|
|
1274
|
+
this.emit({ type: "configure:label:exists", name: label.name });
|
|
1275
|
+
} else {
|
|
1276
|
+
return fail("GITHUB_API_ERROR", `Failed to create label "${label.name}": ${error instanceof Error ? error.message : error}`, true);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return ok({ created, deleted: [], skipped });
|
|
1281
|
+
}
|
|
1282
|
+
async deleteLabels(owner, repo) {
|
|
1283
|
+
const deleted = [];
|
|
1284
|
+
const skipped = [];
|
|
1285
|
+
for (const label of FLEET_LABELS) {
|
|
1286
|
+
try {
|
|
1287
|
+
await this.octokit.rest.issues.deleteLabel({
|
|
1288
|
+
owner,
|
|
1289
|
+
repo,
|
|
1290
|
+
name: label.name
|
|
1291
|
+
});
|
|
1292
|
+
deleted.push(label.name);
|
|
1293
|
+
this.emit({ type: "configure:label:created", name: label.name });
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
1296
|
+
if (status === 404) {
|
|
1297
|
+
skipped.push(label.name);
|
|
1298
|
+
this.emit({ type: "configure:label:exists", name: label.name });
|
|
1299
|
+
} else {
|
|
1300
|
+
return fail("GITHUB_API_ERROR", `Failed to delete label "${label.name}": ${error instanceof Error ? error.message : error}`, true);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return ok({ created: [], deleted, skipped });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
// src/analyze/spec.ts
|
|
1308
|
+
import { z as z9 } from "zod";
|
|
1309
|
+
var AnalyzeInputSchema = z9.object({
|
|
1310
|
+
goal: z9.string().optional(),
|
|
1311
|
+
goalsDir: z9.string().default(".fleet/goals"),
|
|
1312
|
+
milestone: z9.string().optional(),
|
|
1313
|
+
owner: z9.string().min(1),
|
|
1314
|
+
repo: z9.string().min(1),
|
|
1315
|
+
baseBranch: z9.string().default("main")
|
|
1316
|
+
});
|
|
1317
|
+
var AnalyzeErrorCode = z9.enum([
|
|
1318
|
+
"GOAL_NOT_FOUND",
|
|
1319
|
+
"NO_GOALS_FOUND",
|
|
1320
|
+
"MILESTONE_FETCH_FAILED",
|
|
1321
|
+
"SESSION_DISPATCH_FAILED",
|
|
1322
|
+
"UNKNOWN_ERROR"
|
|
1323
|
+
]);
|
|
1324
|
+
// src/analyze/triage-prompt.ts
|
|
1325
|
+
var TRIAGE_GOAL_FILENAME = "triage.md";
|
|
1326
|
+
function getBuiltInTriagePrompt(repoFullName) {
|
|
1327
|
+
return `# Triage
|
|
1328
|
+
|
|
1329
|
+
Triage all open issues in **${repoFullName}** that are not assigned to a milestone.
|
|
1330
|
+
For each, determine actionability, root cause, priority, and grouping potential.
|
|
1331
|
+
|
|
1332
|
+
## Insight Hints
|
|
1333
|
+
- For non-actionable issues, report them as insights with the reason they
|
|
1334
|
+
were skipped and the suggested owner.
|
|
1335
|
+
|
|
1336
|
+
## Constraints
|
|
1337
|
+
- Only create signals for work that does NOT already have an open issue or recent PR.
|
|
1338
|
+
- Every assessment must include a \`Target Files\` section listing the exact files a worker agent should modify.
|
|
1339
|
+
- Keep tasks small and isolated — one logical change per issue.
|
|
1340
|
+
- Apply the \`fleet\` tag to every created signal.
|
|
1341
|
+
- If multiple issues share the same root cause or touch the same files, group them into a single task to avoid merge conflicts.
|
|
1342
|
+
`;
|
|
1343
|
+
}
|
|
1344
|
+
// src/analyze/handler.ts
|
|
1345
|
+
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
1346
|
+
import { basename } from "path";
|
|
1347
|
+
import { globSync } from "glob";
|
|
1348
|
+
|
|
1349
|
+
// src/analyze/goals.ts
|
|
1350
|
+
import { readFileSync } from "fs";
|
|
1351
|
+
import { parse as parseYaml } from "yaml";
|
|
1352
|
+
function parseGoalFile(filePath) {
|
|
1353
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
1354
|
+
return parseGoalContent(raw);
|
|
1355
|
+
}
|
|
1356
|
+
function parseGoalContent(content) {
|
|
1357
|
+
const trimmed = content.trim();
|
|
1358
|
+
if (!trimmed.startsWith("---")) {
|
|
1359
|
+
return { config: {}, body: trimmed };
|
|
1360
|
+
}
|
|
1361
|
+
const closingIndex = trimmed.indexOf(`
|
|
1362
|
+
---`, 3);
|
|
1363
|
+
if (closingIndex === -1) {
|
|
1364
|
+
return { config: {}, body: trimmed };
|
|
1365
|
+
}
|
|
1366
|
+
const frontmatterRaw = trimmed.slice(4, closingIndex).trim();
|
|
1367
|
+
const body = trimmed.slice(closingIndex + 4).trim();
|
|
1368
|
+
const parsed = frontmatterRaw ? parseYaml(frontmatterRaw) ?? {} : {};
|
|
1369
|
+
return {
|
|
1370
|
+
config: {
|
|
1371
|
+
milestone: parsed.milestone?.toString()
|
|
1372
|
+
},
|
|
1373
|
+
body
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/analyze/milestone.ts
|
|
1378
|
+
var IGNORE_LABEL = "status: ignore";
|
|
1379
|
+
function isTargetIssue(issue) {
|
|
1380
|
+
if (issue.pull_request)
|
|
1381
|
+
return false;
|
|
1382
|
+
const hasIgnoreLabel = issue.labels?.some((label) => {
|
|
1383
|
+
const labelName = typeof label === "string" ? label : label.name;
|
|
1384
|
+
return labelName === IGNORE_LABEL;
|
|
1385
|
+
});
|
|
1386
|
+
return !hasIgnoreLabel;
|
|
1387
|
+
}
|
|
1388
|
+
function toMilestoneIssue(raw) {
|
|
1389
|
+
return {
|
|
1390
|
+
number: raw.number,
|
|
1391
|
+
title: raw.title,
|
|
1392
|
+
state: raw.state,
|
|
1393
|
+
labels: (raw.labels ?? []).map((l) => typeof l === "string" ? l : l.name),
|
|
1394
|
+
body: raw.body ?? "",
|
|
1395
|
+
createdAt: raw.created_at,
|
|
1396
|
+
closedAt: raw.closed_at ?? undefined
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function toPullRequest(raw) {
|
|
1400
|
+
return {
|
|
1401
|
+
number: raw.number,
|
|
1402
|
+
title: raw.title,
|
|
1403
|
+
head: raw.head.ref,
|
|
1404
|
+
base: raw.base.ref,
|
|
1405
|
+
body: raw.body ?? ""
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
async function getMilestoneContext(octokit, options) {
|
|
1409
|
+
const { owner, repo, milestone, closedLookbackDays = 14 } = options;
|
|
1410
|
+
let milestoneInfo;
|
|
1411
|
+
if (milestone) {
|
|
1412
|
+
const { data } = await octokit.rest.issues.getMilestone({
|
|
1413
|
+
owner,
|
|
1414
|
+
repo,
|
|
1415
|
+
milestone_number: parseInt(milestone, 10)
|
|
1416
|
+
});
|
|
1417
|
+
milestoneInfo = { number: data.number, title: data.title };
|
|
1418
|
+
}
|
|
1419
|
+
const apiMilestoneFilter = milestone || "none";
|
|
1420
|
+
const { data: openRaw } = await octokit.rest.issues.listForRepo({
|
|
1421
|
+
owner,
|
|
1422
|
+
repo,
|
|
1423
|
+
state: "open",
|
|
1424
|
+
milestone: apiMilestoneFilter,
|
|
1425
|
+
per_page: 100
|
|
1426
|
+
});
|
|
1427
|
+
const since = new Date;
|
|
1428
|
+
since.setDate(since.getDate() - closedLookbackDays);
|
|
1429
|
+
const { data: closedRaw } = await octokit.rest.issues.listForRepo({
|
|
1430
|
+
owner,
|
|
1431
|
+
repo,
|
|
1432
|
+
state: "closed",
|
|
1433
|
+
milestone: apiMilestoneFilter,
|
|
1434
|
+
since: since.toISOString(),
|
|
1435
|
+
per_page: 100
|
|
1436
|
+
});
|
|
1437
|
+
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
1438
|
+
owner,
|
|
1439
|
+
repo,
|
|
1440
|
+
state: "open",
|
|
1441
|
+
per_page: 100
|
|
1442
|
+
});
|
|
1443
|
+
return {
|
|
1444
|
+
milestone: milestoneInfo,
|
|
1445
|
+
issues: {
|
|
1446
|
+
open: openRaw.filter(isTargetIssue).map(toMilestoneIssue),
|
|
1447
|
+
closed: closedRaw.filter(isTargetIssue).map(toMilestoneIssue)
|
|
1448
|
+
},
|
|
1449
|
+
pullRequests: openPRs.map(toPullRequest)
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/analyze/formatting.ts
|
|
1454
|
+
function toIssueMarkdown(issue) {
|
|
1455
|
+
const lines = [
|
|
1456
|
+
`## #${issue.number}: ${issue.title}`,
|
|
1457
|
+
``,
|
|
1458
|
+
`| Field | Value |`,
|
|
1459
|
+
`|-------|-------|`,
|
|
1460
|
+
`| **State** | ${issue.state}${issue.closedAt ? ` (closed ${issue.closedAt})` : ""} |`,
|
|
1461
|
+
`| **Labels** | ${issue.labels.map((l) => `\`${l}\``).join(", ") || "none"} |`,
|
|
1462
|
+
`| **Created** | ${issue.createdAt} |`
|
|
1463
|
+
];
|
|
1464
|
+
if (issue.closedAt) {
|
|
1465
|
+
lines.push(`| **Closed** | ${issue.closedAt} |`);
|
|
1466
|
+
}
|
|
1467
|
+
lines.push(``);
|
|
1468
|
+
if (issue.body) {
|
|
1469
|
+
lines.push(`### Description`, ``, issue.body.trim(), ``);
|
|
1470
|
+
}
|
|
1471
|
+
lines.push(`---`, ``);
|
|
1472
|
+
return lines.join(`
|
|
1473
|
+
`);
|
|
1474
|
+
}
|
|
1475
|
+
function toIssueLean(issue) {
|
|
1476
|
+
return `- #${issue.number}: ${issue.title} [${issue.state}]`;
|
|
1477
|
+
}
|
|
1478
|
+
function formatPRContext(pr) {
|
|
1479
|
+
const fixesPattern = /(?:fixes|closes|resolves)\s+#(\d+)/gi;
|
|
1480
|
+
const linkedIssues = [];
|
|
1481
|
+
let match;
|
|
1482
|
+
while ((match = fixesPattern.exec(pr.body || "")) !== null) {
|
|
1483
|
+
linkedIssues.push(`#${match[1]}`);
|
|
1484
|
+
}
|
|
1485
|
+
const links = linkedIssues.length > 0 ? ` → Fixes ${linkedIssues.join(", ")}` : "";
|
|
1486
|
+
return `- PR #${pr.number}: ${pr.title}${links}
|
|
1487
|
+
Head: ${pr.head || "?"} → Base: ${pr.base || "?"}
|
|
1488
|
+
`;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/analyze/prompt.ts
|
|
1492
|
+
var SYSTEM_PREAMBLE = `You are a senior software architect performing a rigorous code analysis against a set of goal directives. Your job is to identify gaps and create GitHub issues for each actionable task.`;
|
|
1493
|
+
var DEDUP_RULES = `**Deduplication Rules (MANDATORY):**
|
|
1494
|
+
1. **Closed Issue = Work Already Done.** Do NOT recreate issues that have been closed.
|
|
1495
|
+
2. **Open Issue = Work In Progress.** Someone is already handling it. Do NOT create a duplicate.
|
|
1496
|
+
3. **PR with "Fixes #N" = Issue Being Resolved.** If a PR references an issue, that issue is handled. Do NOT create a new issue for the same thing.
|
|
1497
|
+
4. **When in doubt, skip it.** It is better to miss a real gap than to create a duplicate that wastes engineering time.`;
|
|
1498
|
+
var PHASE_0_VERIFY = `### Phase 0: Reality Verification
|
|
1499
|
+
Before planning new tasks, verify the current state of the repository against your goal directives.
|
|
1500
|
+
|
|
1501
|
+
The full details of every open and recently closed issue are provided above in "Historical Context (The Map)". Read each issue's **Objective** and **Proposed Implementation** sections carefully to understand what work is already planned or completed.
|
|
1502
|
+
|
|
1503
|
+
Then, inspect the actual source files to confirm whether the required changes or architectural gaps currently exist. Cross-reference your findings with both the Open and Recently Closed Issues to ensure the work has not already been completed or is not already tracked.
|
|
1504
|
+
|
|
1505
|
+
Proceed to Phase 1 exclusively for gaps that are:
|
|
1506
|
+
1. Demonstrably present in the live codebase today, AND
|
|
1507
|
+
2. Not already covered by an existing open issue's Objective.`;
|
|
1508
|
+
var PHASE_1_INVESTIGATE = `### Phase 1: Investigate
|
|
1509
|
+
Trace identified gaps directly to their source in the codebase. Produce a code-level diagnosis.
|
|
1510
|
+
|
|
1511
|
+
For every identified issue, you must:
|
|
1512
|
+
1. **Identify the exact code path:** Map the execution flow referencing specific files, functions, and line ranges.
|
|
1513
|
+
2. **Explain the mechanism:** Show the relevant code snippet from the existing codebase and annotate exactly where the logic diverges from the goal directives.
|
|
1514
|
+
3. **Determine the root cause:** Classify the gap (e.g., architectural mismatch, schema update, missing logic).`;
|
|
1515
|
+
var PHASE_2_ARCHITECT = `### Phase 2: Architect
|
|
1516
|
+
Design a concrete, production-ready solution for each root cause.
|
|
1517
|
+
|
|
1518
|
+
For each solution, you must provide:
|
|
1519
|
+
1. **Proposed Implementation:** Write the actual TypeScript/code demonstrating the solution. Include function signatures, interfaces, and logic.
|
|
1520
|
+
2. **Integration Points:** Detail exactly where in the existing code this gets wired in, using before/after diffs to show the structural changes.
|
|
1521
|
+
3. **Edge Cases:** Identify assumptions and define fallback behaviors.
|
|
1522
|
+
4. **Test Scenarios:** Define specific test cases, inputs, and expected outputs that validate the fix.`;
|
|
1523
|
+
var PHASE_3_PLAN = `### Phase 3: Plan (Coupling & Boundary Analysis)
|
|
1524
|
+
Evaluate the exact file requirements for each architectural solution to define strict boundaries for the downstream worker agents.
|
|
1525
|
+
|
|
1526
|
+
1. **Coupling Analysis:** Map all implicitly coupled files. Identify test files that exercise the modified code, barrel exports (\`index.ts\`), and shared utilities.
|
|
1527
|
+
2. **File Ownership & Locking:** The downstream Orchestrator prevents merge conflicts by locking files at runtime. You must exhaustively list every single file (source, test, and utility) the worker agent needs to touch.
|
|
1528
|
+
3. **Task Sizing:** Keep tasks strictly isolated by functional domain. You may assign the same core file (e.g., \`types.ts\`) to multiple tasks; the Orchestrator will sequence them automatically based on your exhaustive file list.`;
|
|
1529
|
+
var PHASE_4_DISPATCH_HEALTHY = `**Path A: System Healthy Protocol**
|
|
1530
|
+
If your Phase 0 verification confirms that the codebase already satisfies the goal directives and no actionable work remains, formulate a comprehensive "Fleet Status: Goal Currently Satisfied" report. Detail exactly which domains, files, and logic paths you verified to reach this conclusion. Output this markdown report and exit gracefully, bypassing the issue creation step entirely. Preserve the repository's current state by creating issues only when substantial, goal-aligned engineering work is required.`;
|
|
1531
|
+
var PHASE_4_DISPATCH_ISSUES = `**Path B: Task Dispatch Protocol**
|
|
1532
|
+
For EACH validated task that requires execution, perform a **Deduplication Check** before creating an issue:
|
|
1533
|
+
|
|
1534
|
+
1. Compare your proposed task's scope against EVERY open issue in the Historical Context above.
|
|
1535
|
+
2. If an existing open issue's **Objective** already covers the same gap (even partially or under a different name), **do NOT create a new issue**. Instead, note the overlap in your report and skip creation.
|
|
1536
|
+
3. Only create an issue for gaps that are genuinely novel — not covered by any existing open issue.
|
|
1537
|
+
|
|
1538
|
+
For each non-duplicate task, create a signal using the \`jules-fleet signal create\` command.`;
|
|
1539
|
+
var ISSUE_BODY_TEMPLATE = `\`\`\`markdown
|
|
1540
|
+
### Objective
|
|
1541
|
+
[2-3 sentences explaining the functional goal of this isolated task]
|
|
1542
|
+
|
|
1543
|
+
### Code-Level Diagnosis
|
|
1544
|
+
**Code path:** [e.g., \`src/session.ts → fetch()\`]
|
|
1545
|
+
**Mechanism:** [Explanation of the current state]
|
|
1546
|
+
**Root cause:** [Summary of the architectural gap]
|
|
1547
|
+
|
|
1548
|
+
#### Current Implementation
|
|
1549
|
+
\\\`\\\`\\\`typescript
|
|
1550
|
+
// [Insert exact snippet from current codebase showing the logic to be changed]
|
|
1551
|
+
\\\`\\\`\\\`
|
|
1552
|
+
|
|
1553
|
+
### Proposed Implementation
|
|
1554
|
+
**Files to modify:** [Brief summary of structural changes]
|
|
1555
|
+
|
|
1556
|
+
#### Integration (Before → After)
|
|
1557
|
+
\\\`\\\`\\\`diff
|
|
1558
|
+
// [Insert precise diffs showing how the new logic integrates]
|
|
1559
|
+
\\\`\\\`\\\`
|
|
1560
|
+
|
|
1561
|
+
### Test Scenarios
|
|
1562
|
+
1. [Scenario 1: Input -> Expected Output]
|
|
1563
|
+
2. [Scenario 2: Input -> Expected Output]
|
|
1564
|
+
|
|
1565
|
+
### Target Files
|
|
1566
|
+
- [exact/path/to/source1.ts]
|
|
1567
|
+
- [exact/path/to/source2.ts]
|
|
1568
|
+
- [exact/path/to/test1.test.ts]
|
|
1569
|
+
|
|
1570
|
+
### Boundary Rules
|
|
1571
|
+
Restrict your modifications exclusively to the files listed in the Target Files section. Ensure your source changes are entirely backward-compatible if unowned tests outside your boundary fail. Retain all existing file names and locations outside your explicitly declared target list.
|
|
1572
|
+
\`\`\``;
|
|
1573
|
+
function buildAnalyzerPrompt(options) {
|
|
1574
|
+
const {
|
|
1575
|
+
goalInstructions,
|
|
1576
|
+
openContext,
|
|
1577
|
+
closedContext,
|
|
1578
|
+
prContext,
|
|
1579
|
+
milestoneTitle
|
|
1580
|
+
} = options;
|
|
1581
|
+
const prSection = prContext ? `
|
|
1582
|
+
**Recent Pull Requests (shows what code changes are in flight or merged):**
|
|
1583
|
+
${prContext}
|
|
1584
|
+
` : "";
|
|
1585
|
+
const milestoneFlag = milestoneTitle ? ` \\
|
|
1586
|
+
--scope "${milestoneTitle}"` : "";
|
|
1587
|
+
const cliFormat = `**Signal creation formats:**
|
|
1588
|
+
|
|
1589
|
+
**Assessment** (actionable — requires code changes):
|
|
1590
|
+
\`\`\`bash
|
|
1591
|
+
jules-fleet signal create \\
|
|
1592
|
+
--kind assessment \\
|
|
1593
|
+
--title "[Fleet Execution] <Highly Specific Domain Task Title>" \\
|
|
1594
|
+
--tag fleet \\
|
|
1595
|
+
--body-file <path_to_markdown_file>${milestoneFlag}
|
|
1596
|
+
\`\`\`
|
|
1597
|
+
|
|
1598
|
+
**Insight** (informational — no action required):
|
|
1599
|
+
\`\`\`bash
|
|
1600
|
+
jules-fleet signal create \\
|
|
1601
|
+
--kind insight \\
|
|
1602
|
+
--title "<Descriptive Finding>" \\
|
|
1603
|
+
--tag fleet \\
|
|
1604
|
+
--body-file <path_to_markdown_file>${milestoneFlag}
|
|
1605
|
+
\`\`\``;
|
|
1606
|
+
return [
|
|
1607
|
+
SYSTEM_PREAMBLE,
|
|
1608
|
+
"",
|
|
1609
|
+
`## Your Goal & Directives`,
|
|
1610
|
+
goalInstructions,
|
|
1611
|
+
"",
|
|
1612
|
+
`---`,
|
|
1613
|
+
"",
|
|
1614
|
+
`## Historical Context (The Map)`,
|
|
1615
|
+
"",
|
|
1616
|
+
`The following are the **full details** of every issue in the milestone. Use these to understand the exact scope of existing and completed work. You MUST cross-reference your findings against these issues to avoid creating duplicates.`,
|
|
1617
|
+
"",
|
|
1618
|
+
DEDUP_RULES,
|
|
1619
|
+
"",
|
|
1620
|
+
`**Open Issues:**`,
|
|
1621
|
+
openContext,
|
|
1622
|
+
"",
|
|
1623
|
+
`**Recently Closed Issues (Last 14 Days — these are COMPLETED, do not recreate):**`,
|
|
1624
|
+
closedContext,
|
|
1625
|
+
prSection,
|
|
1626
|
+
`---`,
|
|
1627
|
+
"",
|
|
1628
|
+
`## Your Methodology`,
|
|
1629
|
+
"",
|
|
1630
|
+
`Perform a rigorous multi-phase analysis: **Verify**, **Investigate**, **Architect**, **Plan**, and **Dispatch**.`,
|
|
1631
|
+
"",
|
|
1632
|
+
PHASE_0_VERIFY,
|
|
1633
|
+
"",
|
|
1634
|
+
PHASE_1_INVESTIGATE,
|
|
1635
|
+
"",
|
|
1636
|
+
PHASE_2_ARCHITECT,
|
|
1637
|
+
"",
|
|
1638
|
+
PHASE_3_PLAN,
|
|
1639
|
+
"",
|
|
1640
|
+
`### Phase 4: Dispatch (Issue Creation or Goal Validation)`,
|
|
1641
|
+
`Translate your analysis into independent signals using \`jules-fleet signal create\`, or provide a clean bill of health.`,
|
|
1642
|
+
"",
|
|
1643
|
+
PHASE_4_DISPATCH_HEALTHY,
|
|
1644
|
+
"",
|
|
1645
|
+
PHASE_4_DISPATCH_ISSUES,
|
|
1646
|
+
"",
|
|
1647
|
+
cliFormat,
|
|
1648
|
+
"",
|
|
1649
|
+
`**Required Issue Body Format:**`,
|
|
1650
|
+
`The issue body MUST follow this exact markdown structure to ensure the worker agent and the Orchestrator function correctly:`,
|
|
1651
|
+
"",
|
|
1652
|
+
ISSUE_BODY_TEMPLATE
|
|
1653
|
+
].join(`
|
|
1654
|
+
`);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/analyze/handler.ts
|
|
1658
|
+
class AnalyzeHandler {
|
|
1659
|
+
octokit;
|
|
1660
|
+
dispatcher;
|
|
1661
|
+
emit;
|
|
1662
|
+
constructor(deps) {
|
|
1663
|
+
this.octokit = deps.octokit;
|
|
1664
|
+
this.dispatcher = deps.dispatcher;
|
|
1665
|
+
this.emit = deps.emit ?? (() => {});
|
|
1666
|
+
}
|
|
1667
|
+
async execute(input) {
|
|
1668
|
+
try {
|
|
1669
|
+
const goalFiles = this.resolveGoalFiles(input);
|
|
1670
|
+
if (goalFiles.length === 0) {
|
|
1671
|
+
return fail("NO_GOALS_FOUND", `No goal files found in ${input.goalsDir}/`, true, "Create a .md file in .fleet/goals/ or pass --goal <path>");
|
|
1672
|
+
}
|
|
1673
|
+
this.emit({
|
|
1674
|
+
type: "analyze:start",
|
|
1675
|
+
owner: input.owner,
|
|
1676
|
+
repo: input.repo,
|
|
1677
|
+
goalCount: goalFiles.length
|
|
1678
|
+
});
|
|
1679
|
+
const sessionsStarted = [];
|
|
1680
|
+
for (let i = 0;i < goalFiles.length; i++) {
|
|
1681
|
+
const goalFile = goalFiles[i];
|
|
1682
|
+
const result = await this.processGoal(goalFile, input, i + 1, goalFiles.length);
|
|
1683
|
+
if (result) {
|
|
1684
|
+
sessionsStarted.push(result);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
this.emit({
|
|
1688
|
+
type: "analyze:done",
|
|
1689
|
+
sessionsStarted: sessionsStarted.length,
|
|
1690
|
+
goalsProcessed: goalFiles.length
|
|
1691
|
+
});
|
|
1692
|
+
return ok({ sessionsStarted });
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
resolveGoalFiles(input) {
|
|
1698
|
+
if (input.goal) {
|
|
1699
|
+
if (!existsSync(input.goal)) {
|
|
1700
|
+
return [];
|
|
1701
|
+
}
|
|
1702
|
+
return [input.goal];
|
|
1703
|
+
}
|
|
1704
|
+
const userGoals = globSync(`${input.goalsDir}/*.md`);
|
|
1705
|
+
const hasUserTriage = userGoals.some((f) => basename(f) === TRIAGE_GOAL_FILENAME);
|
|
1706
|
+
if (!hasUserTriage) {
|
|
1707
|
+
userGoals.push(`__builtin__:${TRIAGE_GOAL_FILENAME}`);
|
|
1708
|
+
}
|
|
1709
|
+
return userGoals;
|
|
1710
|
+
}
|
|
1711
|
+
async processGoal(goalFile, input, index, total) {
|
|
1712
|
+
const isBuiltIn = goalFile.startsWith("__builtin__:");
|
|
1713
|
+
let goal;
|
|
1714
|
+
let goalInstructions;
|
|
1715
|
+
if (isBuiltIn) {
|
|
1716
|
+
const repoFullName = `${input.owner}/${input.repo}`;
|
|
1717
|
+
goalInstructions = getBuiltInTriagePrompt(repoFullName);
|
|
1718
|
+
goal = parseGoalContent(goalInstructions);
|
|
1719
|
+
} else {
|
|
1720
|
+
goal = parseGoalFile(goalFile);
|
|
1721
|
+
goalInstructions = readFileSync2(goalFile, "utf-8");
|
|
1722
|
+
}
|
|
1723
|
+
const displayName = isBuiltIn ? `triage.md (built-in)` : basename(goalFile);
|
|
1724
|
+
const milestoneId = input.milestone ?? goal.config?.milestone?.toString();
|
|
1725
|
+
this.emit({
|
|
1726
|
+
type: "analyze:goal:start",
|
|
1727
|
+
file: displayName,
|
|
1728
|
+
index,
|
|
1729
|
+
total,
|
|
1730
|
+
milestone: milestoneId
|
|
1731
|
+
});
|
|
1732
|
+
const ctx = await getMilestoneContext(this.octokit, {
|
|
1733
|
+
owner: input.owner,
|
|
1734
|
+
repo: input.repo,
|
|
1735
|
+
milestone: milestoneId
|
|
1736
|
+
});
|
|
1737
|
+
if (ctx.milestone?.title) {
|
|
1738
|
+
this.emit({
|
|
1739
|
+
type: "analyze:milestone:resolved",
|
|
1740
|
+
title: ctx.milestone.title,
|
|
1741
|
+
id: milestoneId
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
this.emit({
|
|
1745
|
+
type: "analyze:context:fetched",
|
|
1746
|
+
openIssues: ctx.issues.open.length,
|
|
1747
|
+
closedIssues: ctx.issues.closed.length,
|
|
1748
|
+
prs: ctx.pullRequests.length
|
|
1749
|
+
});
|
|
1750
|
+
const openContext = ctx.issues.open.map(toIssueMarkdown).join(`
|
|
1751
|
+
`) || "None.";
|
|
1752
|
+
const closedContext = ctx.issues.closed.map(toIssueMarkdown).join(`
|
|
1753
|
+
`) || "None.";
|
|
1754
|
+
const prContext = ctx.pullRequests.map(formatPRContext).join(`
|
|
1755
|
+
`) || "None.";
|
|
1756
|
+
const prompt = buildAnalyzerPrompt({
|
|
1757
|
+
goalInstructions,
|
|
1758
|
+
openContext,
|
|
1759
|
+
closedContext,
|
|
1760
|
+
prContext,
|
|
1761
|
+
milestoneTitle: ctx.milestone?.title,
|
|
1762
|
+
milestoneId
|
|
1763
|
+
});
|
|
1764
|
+
this.emit({ type: "analyze:session:dispatching", goal: displayName });
|
|
1765
|
+
try {
|
|
1766
|
+
const session = await this.dispatcher.dispatch({
|
|
1767
|
+
prompt,
|
|
1768
|
+
source: {
|
|
1769
|
+
github: `${input.owner}/${input.repo}`,
|
|
1770
|
+
baseBranch: input.baseBranch
|
|
1771
|
+
},
|
|
1772
|
+
requireApproval: false,
|
|
1773
|
+
autoPr: false
|
|
1774
|
+
});
|
|
1775
|
+
this.emit({
|
|
1776
|
+
type: "analyze:session:started",
|
|
1777
|
+
id: session.id,
|
|
1778
|
+
goal: displayName
|
|
1779
|
+
});
|
|
1780
|
+
return { goal: goalFile, sessionId: session.id };
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
this.emit({
|
|
1783
|
+
type: "analyze:session:failed",
|
|
1784
|
+
goal: displayName,
|
|
1785
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1786
|
+
});
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
// src/dispatch/spec.ts
|
|
1792
|
+
import { z as z10 } from "zod";
|
|
1793
|
+
var DispatchInputSchema = z10.object({
|
|
1794
|
+
milestone: z10.string().min(1),
|
|
1795
|
+
owner: z10.string().min(1),
|
|
1796
|
+
repo: z10.string().min(1),
|
|
1797
|
+
baseBranch: z10.string().default("main")
|
|
1798
|
+
});
|
|
1799
|
+
var DispatchErrorCode = z10.enum([
|
|
1800
|
+
"NO_FLEET_ISSUES",
|
|
1801
|
+
"MILESTONE_FETCH_FAILED",
|
|
1802
|
+
"SESSION_DISPATCH_FAILED",
|
|
1803
|
+
"UNKNOWN_ERROR"
|
|
1804
|
+
]);
|
|
1805
|
+
// src/dispatch/status.ts
|
|
1806
|
+
var DISPATCH_MARKER = "\uD83E\uDD16 **Fleet Dispatch Event**";
|
|
1807
|
+
async function getDispatchStatus(octokit, owner, repo, issueNumbers) {
|
|
1808
|
+
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
1809
|
+
owner,
|
|
1810
|
+
repo,
|
|
1811
|
+
state: "open",
|
|
1812
|
+
per_page: 100
|
|
1813
|
+
});
|
|
1814
|
+
const results = [];
|
|
1815
|
+
for (const issueNumber of issueNumbers) {
|
|
1816
|
+
const { data: issue } = await octokit.rest.issues.get({
|
|
1817
|
+
owner,
|
|
1818
|
+
repo,
|
|
1819
|
+
issue_number: issueNumber
|
|
1820
|
+
});
|
|
1821
|
+
const { data: comments } = await octokit.rest.issues.listComments({
|
|
1822
|
+
owner,
|
|
1823
|
+
repo,
|
|
1824
|
+
issue_number: issueNumber,
|
|
1825
|
+
per_page: 100
|
|
1826
|
+
});
|
|
1827
|
+
const dispatchComment = comments.find((c) => c.body?.includes(DISPATCH_MARKER));
|
|
1828
|
+
let dispatchEvent = null;
|
|
1829
|
+
if (dispatchComment?.body) {
|
|
1830
|
+
const sessionMatch = dispatchComment.body.match(/Session:\s*`([^`]+)`/);
|
|
1831
|
+
dispatchEvent = {
|
|
1832
|
+
sessionId: sessionMatch?.[1] ?? "unknown",
|
|
1833
|
+
timestamp: dispatchComment.created_at,
|
|
1834
|
+
commentId: dispatchComment.id
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
const linkedPRs = openPRs.filter((pr) => pr.body?.includes(`#${issueNumber}`) || pr.body?.includes(`Fixes #${issueNumber}`) || pr.body?.includes(`Closes #${issueNumber}`)).map((pr) => pr.number);
|
|
1838
|
+
results.push({
|
|
1839
|
+
number: issueNumber,
|
|
1840
|
+
open: issue.state === "open",
|
|
1841
|
+
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name),
|
|
1842
|
+
dispatchEvent,
|
|
1843
|
+
linkedPRs
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
return results;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/dispatch/events.ts
|
|
1850
|
+
async function recordDispatch(octokit, owner, repo, issueNumber, sessionId) {
|
|
1851
|
+
const timestamp = new Date().toISOString();
|
|
1852
|
+
const body = [
|
|
1853
|
+
`\uD83E\uDD16 **Fleet Dispatch Event**`,
|
|
1854
|
+
`Session: \`${sessionId}\``,
|
|
1855
|
+
`Timestamp: ${timestamp}`
|
|
1856
|
+
].join(`
|
|
1857
|
+
`);
|
|
1858
|
+
const { data: comment } = await octokit.rest.issues.createComment({
|
|
1859
|
+
owner,
|
|
1860
|
+
repo,
|
|
1861
|
+
issue_number: issueNumber,
|
|
1862
|
+
body
|
|
1863
|
+
});
|
|
1864
|
+
return {
|
|
1865
|
+
commentId: comment.id,
|
|
1866
|
+
timestamp
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/dispatch/handler.ts
|
|
1871
|
+
class DispatchHandler {
|
|
1872
|
+
octokit;
|
|
1873
|
+
dispatcher;
|
|
1874
|
+
emit;
|
|
1875
|
+
constructor(deps) {
|
|
1876
|
+
this.octokit = deps.octokit;
|
|
1877
|
+
this.dispatcher = deps.dispatcher;
|
|
1878
|
+
this.emit = deps.emit ?? (() => {});
|
|
1879
|
+
}
|
|
1880
|
+
async execute(input) {
|
|
1881
|
+
try {
|
|
1882
|
+
this.emit({ type: "dispatch:start", milestone: input.milestone });
|
|
1883
|
+
this.emit({ type: "dispatch:scanning" });
|
|
1884
|
+
const ctx = await getMilestoneContext(this.octokit, {
|
|
1885
|
+
owner: input.owner,
|
|
1886
|
+
repo: input.repo,
|
|
1887
|
+
milestone: input.milestone
|
|
1888
|
+
});
|
|
1889
|
+
const fleetIssues = ctx.issues.open.filter((issue) => issue.labels.includes("fleet"));
|
|
1890
|
+
if (fleetIssues.length === 0) {
|
|
1891
|
+
this.emit({ type: "dispatch:done", dispatched: 0, skipped: 0 });
|
|
1892
|
+
return ok({ dispatched: [], skipped: 0 });
|
|
1893
|
+
}
|
|
1894
|
+
const statuses = await getDispatchStatus(this.octokit, input.owner, input.repo, fleetIssues.map((i) => i.number));
|
|
1895
|
+
const undispatched = statuses.filter((s) => s.open && !s.dispatchEvent && s.linkedPRs.length === 0);
|
|
1896
|
+
const skipped = statuses.length - undispatched.length;
|
|
1897
|
+
if (undispatched.length === 0) {
|
|
1898
|
+
this.emit({ type: "dispatch:done", dispatched: 0, skipped });
|
|
1899
|
+
return ok({ dispatched: [], skipped });
|
|
1900
|
+
}
|
|
1901
|
+
this.emit({ type: "dispatch:found", count: undispatched.length });
|
|
1902
|
+
const dispatched = [];
|
|
1903
|
+
for (const status of undispatched) {
|
|
1904
|
+
const issue = fleetIssues.find((i) => i.number === status.number);
|
|
1905
|
+
this.emit({
|
|
1906
|
+
type: "dispatch:issue:dispatching",
|
|
1907
|
+
number: issue.number,
|
|
1908
|
+
title: issue.title
|
|
1909
|
+
});
|
|
1910
|
+
const workerPrompt = `Fix Issue #${issue.number}: ${issue.title}
|
|
1911
|
+
|
|
1912
|
+
IMPORTANT: Your PR title MUST start with "Fixes #${issue.number}" and your PR description MUST include "Fixes #${issue.number}" on its own line so the issue is auto-closed on merge.
|
|
1913
|
+
|
|
1914
|
+
You are an autonomous execution agent. Implement the fix described below exactly as specified.
|
|
1915
|
+
|
|
1916
|
+
---
|
|
1917
|
+
|
|
1918
|
+
${issue.body}
|
|
1919
|
+
`;
|
|
1920
|
+
try {
|
|
1921
|
+
const session = await this.dispatcher.dispatch({
|
|
1922
|
+
prompt: workerPrompt,
|
|
1923
|
+
source: {
|
|
1924
|
+
github: `${input.owner}/${input.repo}`,
|
|
1925
|
+
baseBranch: input.baseBranch
|
|
1926
|
+
},
|
|
1927
|
+
requireApproval: false,
|
|
1928
|
+
autoPr: true
|
|
1929
|
+
});
|
|
1930
|
+
await recordDispatch(this.octokit, input.owner, input.repo, issue.number, session.id);
|
|
1931
|
+
dispatched.push({
|
|
1932
|
+
issueNumber: issue.number,
|
|
1933
|
+
sessionId: session.id
|
|
1934
|
+
});
|
|
1935
|
+
this.emit({
|
|
1936
|
+
type: "dispatch:issue:dispatched",
|
|
1937
|
+
number: issue.number,
|
|
1938
|
+
sessionId: session.id
|
|
1939
|
+
});
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
this.emit({
|
|
1942
|
+
type: "error",
|
|
1943
|
+
code: "DISPATCH_FAILED",
|
|
1944
|
+
message: `Failed to dispatch #${issue.number}: ${error instanceof Error ? error.message : error}`
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
for (const status of statuses) {
|
|
1949
|
+
if (!undispatched.includes(status)) {
|
|
1950
|
+
this.emit({
|
|
1951
|
+
type: "dispatch:issue:skipped",
|
|
1952
|
+
number: status.number,
|
|
1953
|
+
reason: status.dispatchEvent ? "already dispatched" : "has linked PRs"
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
this.emit({
|
|
1958
|
+
type: "dispatch:done",
|
|
1959
|
+
dispatched: dispatched.length,
|
|
1960
|
+
skipped
|
|
1961
|
+
});
|
|
1962
|
+
return ok({ dispatched, skipped });
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
// src/signal/spec.ts
|
|
1969
|
+
import { z as z11 } from "zod";
|
|
1970
|
+
var SignalKind = z11.enum(["insight", "assessment"]);
|
|
1971
|
+
var SignalCreateInputSchema = z11.object({
|
|
1972
|
+
owner: z11.string().min(1),
|
|
1973
|
+
repo: z11.string().min(1),
|
|
1974
|
+
kind: SignalKind.default("assessment"),
|
|
1975
|
+
title: z11.string().min(1),
|
|
1976
|
+
body: z11.string().min(1),
|
|
1977
|
+
tags: z11.array(z11.string()).default([]),
|
|
1978
|
+
scope: z11.string().optional()
|
|
1979
|
+
});
|
|
1980
|
+
var SignalCreateErrorCode = z11.enum([
|
|
1981
|
+
"GITHUB_API_ERROR",
|
|
1982
|
+
"SCOPE_NOT_FOUND",
|
|
1983
|
+
"UNKNOWN_ERROR"
|
|
1984
|
+
]);
|
|
1985
|
+
// src/signal/handler.ts
|
|
1986
|
+
class SignalCreateHandler {
|
|
1987
|
+
deps;
|
|
1988
|
+
constructor(deps) {
|
|
1989
|
+
this.deps = deps;
|
|
1990
|
+
}
|
|
1991
|
+
async execute(input) {
|
|
1992
|
+
try {
|
|
1993
|
+
const { octokit } = this.deps;
|
|
1994
|
+
const labels = [...input.tags];
|
|
1995
|
+
labels.push(input.kind === "insight" ? "fleet-insight" : "fleet-assessment");
|
|
1996
|
+
let milestoneNumber;
|
|
1997
|
+
if (input.scope) {
|
|
1998
|
+
const { data: milestones } = await octokit.rest.issues.listMilestones({
|
|
1999
|
+
owner: input.owner,
|
|
2000
|
+
repo: input.repo,
|
|
2001
|
+
state: "open"
|
|
2002
|
+
});
|
|
2003
|
+
const match = milestones.find((m) => m.title.toLowerCase() === input.scope.toLowerCase());
|
|
2004
|
+
if (!match) {
|
|
2005
|
+
return {
|
|
2006
|
+
success: false,
|
|
2007
|
+
error: {
|
|
2008
|
+
code: "SCOPE_NOT_FOUND",
|
|
2009
|
+
message: `No open milestone found matching scope "${input.scope}"`,
|
|
2010
|
+
recoverable: true,
|
|
2011
|
+
suggestion: `Create a milestone named "${input.scope}" or omit --scope`
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
milestoneNumber = match.number;
|
|
2016
|
+
}
|
|
2017
|
+
const { data } = await octokit.rest.issues.create({
|
|
2018
|
+
owner: input.owner,
|
|
2019
|
+
repo: input.repo,
|
|
2020
|
+
title: input.title,
|
|
2021
|
+
body: input.body,
|
|
2022
|
+
labels,
|
|
2023
|
+
...milestoneNumber && { milestone: milestoneNumber }
|
|
2024
|
+
});
|
|
2025
|
+
return {
|
|
2026
|
+
success: true,
|
|
2027
|
+
data: { id: data.number, url: data.html_url }
|
|
2028
|
+
};
|
|
2029
|
+
} catch (error) {
|
|
2030
|
+
if (error instanceof Error && "status" in error) {
|
|
2031
|
+
return {
|
|
2032
|
+
success: false,
|
|
2033
|
+
error: {
|
|
2034
|
+
code: "GITHUB_API_ERROR",
|
|
2035
|
+
message: error.message,
|
|
2036
|
+
recoverable: error.status === 403 || error.status === 429,
|
|
2037
|
+
suggestion: error.status === 401 ? "Check your GITHUB_TOKEN or GitHub App credentials" : undefined
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
return {
|
|
2042
|
+
success: false,
|
|
2043
|
+
error: {
|
|
2044
|
+
code: "UNKNOWN_ERROR",
|
|
2045
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2046
|
+
recoverable: false
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
export {
|
|
2053
|
+
toIssueMarkdown,
|
|
2054
|
+
toIssueLean,
|
|
2055
|
+
selectByLabel,
|
|
2056
|
+
selectByFleetRun,
|
|
2057
|
+
resolvePrivateKey,
|
|
2058
|
+
recordDispatch,
|
|
2059
|
+
parseGoalFile,
|
|
2060
|
+
parseGoalContent,
|
|
2061
|
+
parseGitRemoteUrl,
|
|
2062
|
+
ok,
|
|
2063
|
+
isInteractive,
|
|
2064
|
+
getMilestoneContext,
|
|
2065
|
+
getGitRepoInfo,
|
|
2066
|
+
getDispatchStatus,
|
|
2067
|
+
getCurrentBranch,
|
|
2068
|
+
getBuiltInTriagePrompt,
|
|
2069
|
+
getAuthOptions,
|
|
2070
|
+
formatPRContext,
|
|
2071
|
+
fail,
|
|
2072
|
+
createResultSchemas,
|
|
2073
|
+
createRenderer,
|
|
2074
|
+
createFleetOctokit,
|
|
2075
|
+
createEmitter,
|
|
2076
|
+
cachePlugin,
|
|
2077
|
+
buildAnalyzerPrompt,
|
|
2078
|
+
TRIAGE_GOAL_FILENAME,
|
|
2079
|
+
SignalKind as SignalKindEnum,
|
|
2080
|
+
SignalCreateInputSchema,
|
|
2081
|
+
SignalCreateHandler,
|
|
2082
|
+
RepoInfoSchema,
|
|
2083
|
+
PRSchema,
|
|
2084
|
+
MergeMode,
|
|
2085
|
+
MergeInputSchema,
|
|
2086
|
+
MergeHandler,
|
|
2087
|
+
MergeErrorCode,
|
|
2088
|
+
LabelSchema,
|
|
2089
|
+
InitInputSchema,
|
|
2090
|
+
InitHandler,
|
|
2091
|
+
InitErrorCode,
|
|
2092
|
+
FLEET_LABELS,
|
|
2093
|
+
DispatchInputSchema,
|
|
2094
|
+
DispatchHandler,
|
|
2095
|
+
DispatchErrorCode,
|
|
2096
|
+
ConfigureResource,
|
|
2097
|
+
ConfigureInputSchema,
|
|
2098
|
+
ConfigureHandler,
|
|
2099
|
+
ConfigureErrorCode,
|
|
2100
|
+
ConfigureAction,
|
|
2101
|
+
CheckRunSchema,
|
|
2102
|
+
AnalyzeInputSchema,
|
|
2103
|
+
AnalyzeHandler,
|
|
2104
|
+
AnalyzeErrorCode
|
|
2105
|
+
};
|