@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.
Files changed (105) hide show
  1. package/README.md +205 -0
  2. package/dist/analyze/formatting.d.ts +19 -0
  3. package/dist/analyze/goals.d.ts +18 -0
  4. package/dist/analyze/handler.d.ts +23 -0
  5. package/dist/analyze/index.d.ts +8 -0
  6. package/dist/analyze/milestone.d.ts +43 -0
  7. package/dist/analyze/prompt.d.ts +10 -0
  8. package/dist/analyze/spec.d.ts +54 -0
  9. package/dist/analyze/triage-prompt.d.ts +16 -0
  10. package/dist/cli/analyze.command.d.ts +24 -0
  11. package/dist/cli/analyze.command.mjs +1015 -0
  12. package/dist/cli/commands.json +1 -0
  13. package/dist/cli/configure.command.d.ts +21 -0
  14. package/dist/cli/configure.command.mjs +623 -0
  15. package/dist/cli/dispatch.command.d.ts +16 -0
  16. package/dist/cli/dispatch.command.mjs +777 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.mjs +40 -0
  19. package/dist/cli/init.command.d.ts +38 -0
  20. package/dist/cli/init.command.mjs +1287 -0
  21. package/dist/cli/merge.command.d.ts +36 -0
  22. package/dist/cli/merge.command.mjs +859 -0
  23. package/dist/cli/signal.command.d.ts +2 -0
  24. package/dist/cli/signal.command.mjs +288 -0
  25. package/dist/configure/handler.d.ts +19 -0
  26. package/dist/configure/index.d.ts +4 -0
  27. package/dist/configure/labels.d.ts +6 -0
  28. package/dist/configure/spec.d.ts +49 -0
  29. package/dist/dispatch/events.d.ts +12 -0
  30. package/dist/dispatch/handler.d.ts +21 -0
  31. package/dist/dispatch/index.d.ts +5 -0
  32. package/dist/dispatch/spec.d.ts +47 -0
  33. package/dist/dispatch/status.d.ts +24 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.mjs +2105 -0
  36. package/dist/init/handler.d.ts +22 -0
  37. package/dist/init/index.d.ts +4 -0
  38. package/dist/init/ops/commit-files.d.ts +10 -0
  39. package/dist/init/ops/create-branch.d.ts +16 -0
  40. package/dist/init/ops/create-pr.d.ts +15 -0
  41. package/dist/init/ops/pr-body.d.ts +5 -0
  42. package/dist/init/ops/upload-secrets.d.ts +11 -0
  43. package/dist/init/spec.d.ts +50 -0
  44. package/dist/init/templates/analyze.d.ts +2 -0
  45. package/dist/init/templates/dispatch.d.ts +2 -0
  46. package/dist/init/templates/example-goal.d.ts +5 -0
  47. package/dist/init/templates/merge.d.ts +2 -0
  48. package/dist/init/templates/types.d.ts +6 -0
  49. package/dist/init/templates.d.ts +10 -0
  50. package/dist/init/types.d.ts +19 -0
  51. package/dist/init/wizard/headless.d.ts +8 -0
  52. package/dist/init/wizard/index.d.ts +3 -0
  53. package/dist/init/wizard/interactive.d.ts +9 -0
  54. package/dist/init/wizard/types.d.ts +22 -0
  55. package/dist/merge/handler.d.ts +21 -0
  56. package/dist/merge/index.d.ts +5 -0
  57. package/dist/merge/ops/index.d.ts +4 -0
  58. package/dist/merge/ops/redispatch.d.ts +8 -0
  59. package/dist/merge/ops/squash-merge.d.ts +8 -0
  60. package/dist/merge/ops/update-branch.d.ts +11 -0
  61. package/dist/merge/ops/wait-for-ci.d.ts +7 -0
  62. package/dist/merge/select/by-fleet-run.d.ts +8 -0
  63. package/dist/merge/select/by-label.d.ts +7 -0
  64. package/dist/merge/select/index.d.ts +2 -0
  65. package/dist/merge/spec.d.ts +99 -0
  66. package/dist/shared/auth/cache-plugin.d.ts +9 -0
  67. package/dist/shared/auth/git.d.ts +22 -0
  68. package/dist/shared/auth/index.d.ts +4 -0
  69. package/dist/shared/auth/octokit.d.ts +11 -0
  70. package/dist/shared/auth/resolve-key.d.ts +11 -0
  71. package/dist/shared/events/analyze.d.ts +37 -0
  72. package/dist/shared/events/configure.d.ts +21 -0
  73. package/dist/shared/events/dispatch.d.ts +26 -0
  74. package/dist/shared/events/error.d.ts +7 -0
  75. package/dist/shared/events/index.d.ts +16 -0
  76. package/dist/shared/events/init.d.ts +49 -0
  77. package/dist/shared/events/merge.d.ts +72 -0
  78. package/dist/shared/events.d.ts +1 -0
  79. package/dist/shared/index.d.ts +6 -0
  80. package/dist/shared/result/create-result-schemas.d.ts +72 -0
  81. package/dist/shared/result/fail.d.ts +10 -0
  82. package/dist/shared/result/index.d.ts +3 -0
  83. package/dist/shared/result/ok.d.ts +5 -0
  84. package/dist/shared/schemas/check-run.d.ts +16 -0
  85. package/dist/shared/schemas/index.d.ts +4 -0
  86. package/dist/shared/schemas/label.d.ts +16 -0
  87. package/dist/shared/schemas/pr.d.ts +19 -0
  88. package/dist/shared/schemas/repo-info.d.ts +16 -0
  89. package/dist/shared/session-dispatcher.d.ts +18 -0
  90. package/dist/shared/ui/assert-never.d.ts +13 -0
  91. package/dist/shared/ui/index.d.ts +18 -0
  92. package/dist/shared/ui/interactive.d.ts +19 -0
  93. package/dist/shared/ui/plain.d.ts +16 -0
  94. package/dist/shared/ui/render/analyze.d.ts +4 -0
  95. package/dist/shared/ui/render/configure.d.ts +4 -0
  96. package/dist/shared/ui/render/dispatch.d.ts +4 -0
  97. package/dist/shared/ui/render/error.d.ts +4 -0
  98. package/dist/shared/ui/render/init.d.ts +4 -0
  99. package/dist/shared/ui/render/merge.d.ts +4 -0
  100. package/dist/shared/ui/session-url.d.ts +13 -0
  101. package/dist/shared/ui/spec.d.ts +30 -0
  102. package/dist/signal/handler.d.ts +17 -0
  103. package/dist/signal/index.d.ts +3 -0
  104. package/dist/signal/spec.d.ts +60 -0
  105. 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
+ };