@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
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/cli/analyze.command.ts
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
|
|
7
|
+
// src/analyze/spec.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var AnalyzeInputSchema = z.object({
|
|
10
|
+
goal: z.string().optional(),
|
|
11
|
+
goalsDir: z.string().default(".fleet/goals"),
|
|
12
|
+
milestone: z.string().optional(),
|
|
13
|
+
owner: z.string().min(1),
|
|
14
|
+
repo: z.string().min(1),
|
|
15
|
+
baseBranch: z.string().default("main")
|
|
16
|
+
});
|
|
17
|
+
var AnalyzeErrorCode = z.enum([
|
|
18
|
+
"GOAL_NOT_FOUND",
|
|
19
|
+
"NO_GOALS_FOUND",
|
|
20
|
+
"MILESTONE_FETCH_FAILED",
|
|
21
|
+
"SESSION_DISPATCH_FAILED",
|
|
22
|
+
"UNKNOWN_ERROR"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// src/analyze/handler.ts
|
|
26
|
+
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
27
|
+
import { basename } from "path";
|
|
28
|
+
import { globSync } from "glob";
|
|
29
|
+
|
|
30
|
+
// src/shared/result/create-result-schemas.ts
|
|
31
|
+
import { z as z2 } from "zod";
|
|
32
|
+
// src/shared/result/ok.ts
|
|
33
|
+
function ok(data) {
|
|
34
|
+
return { success: true, data };
|
|
35
|
+
}
|
|
36
|
+
// src/shared/result/fail.ts
|
|
37
|
+
function fail(code, message, recoverable = false, suggestion) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: { code, message, recoverable, suggestion }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// src/analyze/goals.ts
|
|
44
|
+
import { readFileSync } from "fs";
|
|
45
|
+
import { parse as parseYaml } from "yaml";
|
|
46
|
+
function parseGoalFile(filePath) {
|
|
47
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
48
|
+
return parseGoalContent(raw);
|
|
49
|
+
}
|
|
50
|
+
function parseGoalContent(content) {
|
|
51
|
+
const trimmed = content.trim();
|
|
52
|
+
if (!trimmed.startsWith("---")) {
|
|
53
|
+
return { config: {}, body: trimmed };
|
|
54
|
+
}
|
|
55
|
+
const closingIndex = trimmed.indexOf(`
|
|
56
|
+
---`, 3);
|
|
57
|
+
if (closingIndex === -1) {
|
|
58
|
+
return { config: {}, body: trimmed };
|
|
59
|
+
}
|
|
60
|
+
const frontmatterRaw = trimmed.slice(4, closingIndex).trim();
|
|
61
|
+
const body = trimmed.slice(closingIndex + 4).trim();
|
|
62
|
+
const parsed = frontmatterRaw ? parseYaml(frontmatterRaw) ?? {} : {};
|
|
63
|
+
return {
|
|
64
|
+
config: {
|
|
65
|
+
milestone: parsed.milestone?.toString()
|
|
66
|
+
},
|
|
67
|
+
body
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/analyze/milestone.ts
|
|
72
|
+
var IGNORE_LABEL = "status: ignore";
|
|
73
|
+
function isTargetIssue(issue) {
|
|
74
|
+
if (issue.pull_request)
|
|
75
|
+
return false;
|
|
76
|
+
const hasIgnoreLabel = issue.labels?.some((label) => {
|
|
77
|
+
const labelName = typeof label === "string" ? label : label.name;
|
|
78
|
+
return labelName === IGNORE_LABEL;
|
|
79
|
+
});
|
|
80
|
+
return !hasIgnoreLabel;
|
|
81
|
+
}
|
|
82
|
+
function toMilestoneIssue(raw) {
|
|
83
|
+
return {
|
|
84
|
+
number: raw.number,
|
|
85
|
+
title: raw.title,
|
|
86
|
+
state: raw.state,
|
|
87
|
+
labels: (raw.labels ?? []).map((l) => typeof l === "string" ? l : l.name),
|
|
88
|
+
body: raw.body ?? "",
|
|
89
|
+
createdAt: raw.created_at,
|
|
90
|
+
closedAt: raw.closed_at ?? undefined
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function toPullRequest(raw) {
|
|
94
|
+
return {
|
|
95
|
+
number: raw.number,
|
|
96
|
+
title: raw.title,
|
|
97
|
+
head: raw.head.ref,
|
|
98
|
+
base: raw.base.ref,
|
|
99
|
+
body: raw.body ?? ""
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function getMilestoneContext(octokit, options) {
|
|
103
|
+
const { owner, repo, milestone, closedLookbackDays = 14 } = options;
|
|
104
|
+
let milestoneInfo;
|
|
105
|
+
if (milestone) {
|
|
106
|
+
const { data } = await octokit.rest.issues.getMilestone({
|
|
107
|
+
owner,
|
|
108
|
+
repo,
|
|
109
|
+
milestone_number: parseInt(milestone, 10)
|
|
110
|
+
});
|
|
111
|
+
milestoneInfo = { number: data.number, title: data.title };
|
|
112
|
+
}
|
|
113
|
+
const apiMilestoneFilter = milestone || "none";
|
|
114
|
+
const { data: openRaw } = await octokit.rest.issues.listForRepo({
|
|
115
|
+
owner,
|
|
116
|
+
repo,
|
|
117
|
+
state: "open",
|
|
118
|
+
milestone: apiMilestoneFilter,
|
|
119
|
+
per_page: 100
|
|
120
|
+
});
|
|
121
|
+
const since = new Date;
|
|
122
|
+
since.setDate(since.getDate() - closedLookbackDays);
|
|
123
|
+
const { data: closedRaw } = await octokit.rest.issues.listForRepo({
|
|
124
|
+
owner,
|
|
125
|
+
repo,
|
|
126
|
+
state: "closed",
|
|
127
|
+
milestone: apiMilestoneFilter,
|
|
128
|
+
since: since.toISOString(),
|
|
129
|
+
per_page: 100
|
|
130
|
+
});
|
|
131
|
+
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
132
|
+
owner,
|
|
133
|
+
repo,
|
|
134
|
+
state: "open",
|
|
135
|
+
per_page: 100
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
milestone: milestoneInfo,
|
|
139
|
+
issues: {
|
|
140
|
+
open: openRaw.filter(isTargetIssue).map(toMilestoneIssue),
|
|
141
|
+
closed: closedRaw.filter(isTargetIssue).map(toMilestoneIssue)
|
|
142
|
+
},
|
|
143
|
+
pullRequests: openPRs.map(toPullRequest)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/analyze/formatting.ts
|
|
148
|
+
function toIssueMarkdown(issue) {
|
|
149
|
+
const lines = [
|
|
150
|
+
`## #${issue.number}: ${issue.title}`,
|
|
151
|
+
``,
|
|
152
|
+
`| Field | Value |`,
|
|
153
|
+
`|-------|-------|`,
|
|
154
|
+
`| **State** | ${issue.state}${issue.closedAt ? ` (closed ${issue.closedAt})` : ""} |`,
|
|
155
|
+
`| **Labels** | ${issue.labels.map((l) => `\`${l}\``).join(", ") || "none"} |`,
|
|
156
|
+
`| **Created** | ${issue.createdAt} |`
|
|
157
|
+
];
|
|
158
|
+
if (issue.closedAt) {
|
|
159
|
+
lines.push(`| **Closed** | ${issue.closedAt} |`);
|
|
160
|
+
}
|
|
161
|
+
lines.push(``);
|
|
162
|
+
if (issue.body) {
|
|
163
|
+
lines.push(`### Description`, ``, issue.body.trim(), ``);
|
|
164
|
+
}
|
|
165
|
+
lines.push(`---`, ``);
|
|
166
|
+
return lines.join(`
|
|
167
|
+
`);
|
|
168
|
+
}
|
|
169
|
+
function formatPRContext(pr) {
|
|
170
|
+
const fixesPattern = /(?:fixes|closes|resolves)\s+#(\d+)/gi;
|
|
171
|
+
const linkedIssues = [];
|
|
172
|
+
let match;
|
|
173
|
+
while ((match = fixesPattern.exec(pr.body || "")) !== null) {
|
|
174
|
+
linkedIssues.push(`#${match[1]}`);
|
|
175
|
+
}
|
|
176
|
+
const links = linkedIssues.length > 0 ? ` → Fixes ${linkedIssues.join(", ")}` : "";
|
|
177
|
+
return `- PR #${pr.number}: ${pr.title}${links}
|
|
178
|
+
Head: ${pr.head || "?"} → Base: ${pr.base || "?"}
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/analyze/prompt.ts
|
|
183
|
+
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.`;
|
|
184
|
+
var DEDUP_RULES = `**Deduplication Rules (MANDATORY):**
|
|
185
|
+
1. **Closed Issue = Work Already Done.** Do NOT recreate issues that have been closed.
|
|
186
|
+
2. **Open Issue = Work In Progress.** Someone is already handling it. Do NOT create a duplicate.
|
|
187
|
+
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.
|
|
188
|
+
4. **When in doubt, skip it.** It is better to miss a real gap than to create a duplicate that wastes engineering time.`;
|
|
189
|
+
var PHASE_0_VERIFY = `### Phase 0: Reality Verification
|
|
190
|
+
Before planning new tasks, verify the current state of the repository against your goal directives.
|
|
191
|
+
|
|
192
|
+
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.
|
|
193
|
+
|
|
194
|
+
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.
|
|
195
|
+
|
|
196
|
+
Proceed to Phase 1 exclusively for gaps that are:
|
|
197
|
+
1. Demonstrably present in the live codebase today, AND
|
|
198
|
+
2. Not already covered by an existing open issue's Objective.`;
|
|
199
|
+
var PHASE_1_INVESTIGATE = `### Phase 1: Investigate
|
|
200
|
+
Trace identified gaps directly to their source in the codebase. Produce a code-level diagnosis.
|
|
201
|
+
|
|
202
|
+
For every identified issue, you must:
|
|
203
|
+
1. **Identify the exact code path:** Map the execution flow referencing specific files, functions, and line ranges.
|
|
204
|
+
2. **Explain the mechanism:** Show the relevant code snippet from the existing codebase and annotate exactly where the logic diverges from the goal directives.
|
|
205
|
+
3. **Determine the root cause:** Classify the gap (e.g., architectural mismatch, schema update, missing logic).`;
|
|
206
|
+
var PHASE_2_ARCHITECT = `### Phase 2: Architect
|
|
207
|
+
Design a concrete, production-ready solution for each root cause.
|
|
208
|
+
|
|
209
|
+
For each solution, you must provide:
|
|
210
|
+
1. **Proposed Implementation:** Write the actual TypeScript/code demonstrating the solution. Include function signatures, interfaces, and logic.
|
|
211
|
+
2. **Integration Points:** Detail exactly where in the existing code this gets wired in, using before/after diffs to show the structural changes.
|
|
212
|
+
3. **Edge Cases:** Identify assumptions and define fallback behaviors.
|
|
213
|
+
4. **Test Scenarios:** Define specific test cases, inputs, and expected outputs that validate the fix.`;
|
|
214
|
+
var PHASE_3_PLAN = `### Phase 3: Plan (Coupling & Boundary Analysis)
|
|
215
|
+
Evaluate the exact file requirements for each architectural solution to define strict boundaries for the downstream worker agents.
|
|
216
|
+
|
|
217
|
+
1. **Coupling Analysis:** Map all implicitly coupled files. Identify test files that exercise the modified code, barrel exports (\`index.ts\`), and shared utilities.
|
|
218
|
+
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.
|
|
219
|
+
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.`;
|
|
220
|
+
var PHASE_4_DISPATCH_HEALTHY = `**Path A: System Healthy Protocol**
|
|
221
|
+
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.`;
|
|
222
|
+
var PHASE_4_DISPATCH_ISSUES = `**Path B: Task Dispatch Protocol**
|
|
223
|
+
For EACH validated task that requires execution, perform a **Deduplication Check** before creating an issue:
|
|
224
|
+
|
|
225
|
+
1. Compare your proposed task's scope against EVERY open issue in the Historical Context above.
|
|
226
|
+
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.
|
|
227
|
+
3. Only create an issue for gaps that are genuinely novel — not covered by any existing open issue.
|
|
228
|
+
|
|
229
|
+
For each non-duplicate task, create a signal using the \`jules-fleet signal create\` command.`;
|
|
230
|
+
var ISSUE_BODY_TEMPLATE = `\`\`\`markdown
|
|
231
|
+
### Objective
|
|
232
|
+
[2-3 sentences explaining the functional goal of this isolated task]
|
|
233
|
+
|
|
234
|
+
### Code-Level Diagnosis
|
|
235
|
+
**Code path:** [e.g., \`src/session.ts → fetch()\`]
|
|
236
|
+
**Mechanism:** [Explanation of the current state]
|
|
237
|
+
**Root cause:** [Summary of the architectural gap]
|
|
238
|
+
|
|
239
|
+
#### Current Implementation
|
|
240
|
+
\\\`\\\`\\\`typescript
|
|
241
|
+
// [Insert exact snippet from current codebase showing the logic to be changed]
|
|
242
|
+
\\\`\\\`\\\`
|
|
243
|
+
|
|
244
|
+
### Proposed Implementation
|
|
245
|
+
**Files to modify:** [Brief summary of structural changes]
|
|
246
|
+
|
|
247
|
+
#### Integration (Before → After)
|
|
248
|
+
\\\`\\\`\\\`diff
|
|
249
|
+
// [Insert precise diffs showing how the new logic integrates]
|
|
250
|
+
\\\`\\\`\\\`
|
|
251
|
+
|
|
252
|
+
### Test Scenarios
|
|
253
|
+
1. [Scenario 1: Input -> Expected Output]
|
|
254
|
+
2. [Scenario 2: Input -> Expected Output]
|
|
255
|
+
|
|
256
|
+
### Target Files
|
|
257
|
+
- [exact/path/to/source1.ts]
|
|
258
|
+
- [exact/path/to/source2.ts]
|
|
259
|
+
- [exact/path/to/test1.test.ts]
|
|
260
|
+
|
|
261
|
+
### Boundary Rules
|
|
262
|
+
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.
|
|
263
|
+
\`\`\``;
|
|
264
|
+
function buildAnalyzerPrompt(options) {
|
|
265
|
+
const {
|
|
266
|
+
goalInstructions,
|
|
267
|
+
openContext,
|
|
268
|
+
closedContext,
|
|
269
|
+
prContext,
|
|
270
|
+
milestoneTitle
|
|
271
|
+
} = options;
|
|
272
|
+
const prSection = prContext ? `
|
|
273
|
+
**Recent Pull Requests (shows what code changes are in flight or merged):**
|
|
274
|
+
${prContext}
|
|
275
|
+
` : "";
|
|
276
|
+
const milestoneFlag = milestoneTitle ? ` \\
|
|
277
|
+
--scope "${milestoneTitle}"` : "";
|
|
278
|
+
const cliFormat = `**Signal creation formats:**
|
|
279
|
+
|
|
280
|
+
**Assessment** (actionable — requires code changes):
|
|
281
|
+
\`\`\`bash
|
|
282
|
+
jules-fleet signal create \\
|
|
283
|
+
--kind assessment \\
|
|
284
|
+
--title "[Fleet Execution] <Highly Specific Domain Task Title>" \\
|
|
285
|
+
--tag fleet \\
|
|
286
|
+
--body-file <path_to_markdown_file>${milestoneFlag}
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
**Insight** (informational — no action required):
|
|
290
|
+
\`\`\`bash
|
|
291
|
+
jules-fleet signal create \\
|
|
292
|
+
--kind insight \\
|
|
293
|
+
--title "<Descriptive Finding>" \\
|
|
294
|
+
--tag fleet \\
|
|
295
|
+
--body-file <path_to_markdown_file>${milestoneFlag}
|
|
296
|
+
\`\`\``;
|
|
297
|
+
return [
|
|
298
|
+
SYSTEM_PREAMBLE,
|
|
299
|
+
"",
|
|
300
|
+
`## Your Goal & Directives`,
|
|
301
|
+
goalInstructions,
|
|
302
|
+
"",
|
|
303
|
+
`---`,
|
|
304
|
+
"",
|
|
305
|
+
`## Historical Context (The Map)`,
|
|
306
|
+
"",
|
|
307
|
+
`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.`,
|
|
308
|
+
"",
|
|
309
|
+
DEDUP_RULES,
|
|
310
|
+
"",
|
|
311
|
+
`**Open Issues:**`,
|
|
312
|
+
openContext,
|
|
313
|
+
"",
|
|
314
|
+
`**Recently Closed Issues (Last 14 Days — these are COMPLETED, do not recreate):**`,
|
|
315
|
+
closedContext,
|
|
316
|
+
prSection,
|
|
317
|
+
`---`,
|
|
318
|
+
"",
|
|
319
|
+
`## Your Methodology`,
|
|
320
|
+
"",
|
|
321
|
+
`Perform a rigorous multi-phase analysis: **Verify**, **Investigate**, **Architect**, **Plan**, and **Dispatch**.`,
|
|
322
|
+
"",
|
|
323
|
+
PHASE_0_VERIFY,
|
|
324
|
+
"",
|
|
325
|
+
PHASE_1_INVESTIGATE,
|
|
326
|
+
"",
|
|
327
|
+
PHASE_2_ARCHITECT,
|
|
328
|
+
"",
|
|
329
|
+
PHASE_3_PLAN,
|
|
330
|
+
"",
|
|
331
|
+
`### Phase 4: Dispatch (Issue Creation or Goal Validation)`,
|
|
332
|
+
`Translate your analysis into independent signals using \`jules-fleet signal create\`, or provide a clean bill of health.`,
|
|
333
|
+
"",
|
|
334
|
+
PHASE_4_DISPATCH_HEALTHY,
|
|
335
|
+
"",
|
|
336
|
+
PHASE_4_DISPATCH_ISSUES,
|
|
337
|
+
"",
|
|
338
|
+
cliFormat,
|
|
339
|
+
"",
|
|
340
|
+
`**Required Issue Body Format:**`,
|
|
341
|
+
`The issue body MUST follow this exact markdown structure to ensure the worker agent and the Orchestrator function correctly:`,
|
|
342
|
+
"",
|
|
343
|
+
ISSUE_BODY_TEMPLATE
|
|
344
|
+
].join(`
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/analyze/triage-prompt.ts
|
|
349
|
+
var TRIAGE_GOAL_FILENAME = "triage.md";
|
|
350
|
+
function getBuiltInTriagePrompt(repoFullName) {
|
|
351
|
+
return `# Triage
|
|
352
|
+
|
|
353
|
+
Triage all open issues in **${repoFullName}** that are not assigned to a milestone.
|
|
354
|
+
For each, determine actionability, root cause, priority, and grouping potential.
|
|
355
|
+
|
|
356
|
+
## Insight Hints
|
|
357
|
+
- For non-actionable issues, report them as insights with the reason they
|
|
358
|
+
were skipped and the suggested owner.
|
|
359
|
+
|
|
360
|
+
## Constraints
|
|
361
|
+
- Only create signals for work that does NOT already have an open issue or recent PR.
|
|
362
|
+
- Every assessment must include a \`Target Files\` section listing the exact files a worker agent should modify.
|
|
363
|
+
- Keep tasks small and isolated — one logical change per issue.
|
|
364
|
+
- Apply the \`fleet\` tag to every created signal.
|
|
365
|
+
- If multiple issues share the same root cause or touch the same files, group them into a single task to avoid merge conflicts.
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/analyze/handler.ts
|
|
370
|
+
class AnalyzeHandler {
|
|
371
|
+
octokit;
|
|
372
|
+
dispatcher;
|
|
373
|
+
emit;
|
|
374
|
+
constructor(deps) {
|
|
375
|
+
this.octokit = deps.octokit;
|
|
376
|
+
this.dispatcher = deps.dispatcher;
|
|
377
|
+
this.emit = deps.emit ?? (() => {});
|
|
378
|
+
}
|
|
379
|
+
async execute(input) {
|
|
380
|
+
try {
|
|
381
|
+
const goalFiles = this.resolveGoalFiles(input);
|
|
382
|
+
if (goalFiles.length === 0) {
|
|
383
|
+
return fail("NO_GOALS_FOUND", `No goal files found in ${input.goalsDir}/`, true, "Create a .md file in .fleet/goals/ or pass --goal <path>");
|
|
384
|
+
}
|
|
385
|
+
this.emit({
|
|
386
|
+
type: "analyze:start",
|
|
387
|
+
owner: input.owner,
|
|
388
|
+
repo: input.repo,
|
|
389
|
+
goalCount: goalFiles.length
|
|
390
|
+
});
|
|
391
|
+
const sessionsStarted = [];
|
|
392
|
+
for (let i = 0;i < goalFiles.length; i++) {
|
|
393
|
+
const goalFile = goalFiles[i];
|
|
394
|
+
const result = await this.processGoal(goalFile, input, i + 1, goalFiles.length);
|
|
395
|
+
if (result) {
|
|
396
|
+
sessionsStarted.push(result);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.emit({
|
|
400
|
+
type: "analyze:done",
|
|
401
|
+
sessionsStarted: sessionsStarted.length,
|
|
402
|
+
goalsProcessed: goalFiles.length
|
|
403
|
+
});
|
|
404
|
+
return ok({ sessionsStarted });
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
resolveGoalFiles(input) {
|
|
410
|
+
if (input.goal) {
|
|
411
|
+
if (!existsSync(input.goal)) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
return [input.goal];
|
|
415
|
+
}
|
|
416
|
+
const userGoals = globSync(`${input.goalsDir}/*.md`);
|
|
417
|
+
const hasUserTriage = userGoals.some((f) => basename(f) === TRIAGE_GOAL_FILENAME);
|
|
418
|
+
if (!hasUserTriage) {
|
|
419
|
+
userGoals.push(`__builtin__:${TRIAGE_GOAL_FILENAME}`);
|
|
420
|
+
}
|
|
421
|
+
return userGoals;
|
|
422
|
+
}
|
|
423
|
+
async processGoal(goalFile, input, index, total) {
|
|
424
|
+
const isBuiltIn = goalFile.startsWith("__builtin__:");
|
|
425
|
+
let goal;
|
|
426
|
+
let goalInstructions;
|
|
427
|
+
if (isBuiltIn) {
|
|
428
|
+
const repoFullName = `${input.owner}/${input.repo}`;
|
|
429
|
+
goalInstructions = getBuiltInTriagePrompt(repoFullName);
|
|
430
|
+
goal = parseGoalContent(goalInstructions);
|
|
431
|
+
} else {
|
|
432
|
+
goal = parseGoalFile(goalFile);
|
|
433
|
+
goalInstructions = readFileSync2(goalFile, "utf-8");
|
|
434
|
+
}
|
|
435
|
+
const displayName = isBuiltIn ? `triage.md (built-in)` : basename(goalFile);
|
|
436
|
+
const milestoneId = input.milestone ?? goal.config?.milestone?.toString();
|
|
437
|
+
this.emit({
|
|
438
|
+
type: "analyze:goal:start",
|
|
439
|
+
file: displayName,
|
|
440
|
+
index,
|
|
441
|
+
total,
|
|
442
|
+
milestone: milestoneId
|
|
443
|
+
});
|
|
444
|
+
const ctx = await getMilestoneContext(this.octokit, {
|
|
445
|
+
owner: input.owner,
|
|
446
|
+
repo: input.repo,
|
|
447
|
+
milestone: milestoneId
|
|
448
|
+
});
|
|
449
|
+
if (ctx.milestone?.title) {
|
|
450
|
+
this.emit({
|
|
451
|
+
type: "analyze:milestone:resolved",
|
|
452
|
+
title: ctx.milestone.title,
|
|
453
|
+
id: milestoneId
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
this.emit({
|
|
457
|
+
type: "analyze:context:fetched",
|
|
458
|
+
openIssues: ctx.issues.open.length,
|
|
459
|
+
closedIssues: ctx.issues.closed.length,
|
|
460
|
+
prs: ctx.pullRequests.length
|
|
461
|
+
});
|
|
462
|
+
const openContext = ctx.issues.open.map(toIssueMarkdown).join(`
|
|
463
|
+
`) || "None.";
|
|
464
|
+
const closedContext = ctx.issues.closed.map(toIssueMarkdown).join(`
|
|
465
|
+
`) || "None.";
|
|
466
|
+
const prContext = ctx.pullRequests.map(formatPRContext).join(`
|
|
467
|
+
`) || "None.";
|
|
468
|
+
const prompt = buildAnalyzerPrompt({
|
|
469
|
+
goalInstructions,
|
|
470
|
+
openContext,
|
|
471
|
+
closedContext,
|
|
472
|
+
prContext,
|
|
473
|
+
milestoneTitle: ctx.milestone?.title,
|
|
474
|
+
milestoneId
|
|
475
|
+
});
|
|
476
|
+
this.emit({ type: "analyze:session:dispatching", goal: displayName });
|
|
477
|
+
try {
|
|
478
|
+
const session = await this.dispatcher.dispatch({
|
|
479
|
+
prompt,
|
|
480
|
+
source: {
|
|
481
|
+
github: `${input.owner}/${input.repo}`,
|
|
482
|
+
baseBranch: input.baseBranch
|
|
483
|
+
},
|
|
484
|
+
requireApproval: false,
|
|
485
|
+
autoPr: false
|
|
486
|
+
});
|
|
487
|
+
this.emit({
|
|
488
|
+
type: "analyze:session:started",
|
|
489
|
+
id: session.id,
|
|
490
|
+
goal: displayName
|
|
491
|
+
});
|
|
492
|
+
return { goal: goalFile, sessionId: session.id };
|
|
493
|
+
} catch (error) {
|
|
494
|
+
this.emit({
|
|
495
|
+
type: "analyze:session:failed",
|
|
496
|
+
goal: displayName,
|
|
497
|
+
error: error instanceof Error ? error.message : String(error)
|
|
498
|
+
});
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/shared/auth/octokit.ts
|
|
505
|
+
import { Octokit } from "octokit";
|
|
506
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
507
|
+
|
|
508
|
+
// src/shared/auth/cache-plugin.ts
|
|
509
|
+
function cachePlugin(octokit) {
|
|
510
|
+
const cache = new Map;
|
|
511
|
+
octokit.hook.wrap("request", async (request, options) => {
|
|
512
|
+
const key = `${options.method} ${options.url}`;
|
|
513
|
+
const cached = cache.get(key);
|
|
514
|
+
if (cached) {
|
|
515
|
+
options.headers = {
|
|
516
|
+
...options.headers,
|
|
517
|
+
"if-none-match": cached.etag
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const response = await request(options);
|
|
522
|
+
const etag = response.headers.etag;
|
|
523
|
+
if (etag) {
|
|
524
|
+
cache.set(key, { etag, data: response.data });
|
|
525
|
+
}
|
|
526
|
+
return response;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (error.status === 304 && cached) {
|
|
529
|
+
return { ...error.response, data: cached.data, status: 200 };
|
|
530
|
+
}
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/shared/auth/resolve-key.ts
|
|
537
|
+
function resolvePrivateKey(base64Value, rawValue) {
|
|
538
|
+
if (base64Value) {
|
|
539
|
+
return Buffer.from(base64Value, "base64").toString("utf-8");
|
|
540
|
+
}
|
|
541
|
+
if (rawValue) {
|
|
542
|
+
return rawValue.replace(/\\n/g, `
|
|
543
|
+
`);
|
|
544
|
+
}
|
|
545
|
+
throw new Error("No private key provided. Set GITHUB_APP_PRIVATE_KEY_BASE64 (recommended) or GITHUB_APP_PRIVATE_KEY.");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/shared/auth/octokit.ts
|
|
549
|
+
var CachedOctokit = Octokit.plugin(cachePlugin);
|
|
550
|
+
function getAuthOptions() {
|
|
551
|
+
const appId = process.env.GITHUB_APP_ID;
|
|
552
|
+
const privateKeyBase64 = process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
|
|
553
|
+
const privateKeyRaw = process.env.GITHUB_APP_PRIVATE_KEY;
|
|
554
|
+
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
555
|
+
if (appId && (privateKeyBase64 || privateKeyRaw) && installationId) {
|
|
556
|
+
return {
|
|
557
|
+
authStrategy: createAppAuth,
|
|
558
|
+
auth: {
|
|
559
|
+
appId,
|
|
560
|
+
privateKey: resolvePrivateKey(privateKeyBase64, privateKeyRaw),
|
|
561
|
+
installationId: Number(installationId)
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const token = process.env.GITHUB_TOKEN;
|
|
566
|
+
if (token) {
|
|
567
|
+
return { auth: token };
|
|
568
|
+
}
|
|
569
|
+
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.");
|
|
570
|
+
}
|
|
571
|
+
function createFleetOctokit() {
|
|
572
|
+
return new CachedOctokit(getAuthOptions());
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/shared/auth/git.ts
|
|
576
|
+
import { exec } from "child_process";
|
|
577
|
+
import { promisify } from "util";
|
|
578
|
+
var execAsync = promisify(exec);
|
|
579
|
+
async function getGitRepoInfo(remoteName = "origin") {
|
|
580
|
+
const ghRepo = process.env.GITHUB_REPOSITORY;
|
|
581
|
+
if (ghRepo) {
|
|
582
|
+
const [owner, repo] = ghRepo.split("/");
|
|
583
|
+
return { owner, repo, fullName: ghRepo };
|
|
584
|
+
}
|
|
585
|
+
const { stdout } = await execAsync(`git remote get-url ${remoteName}`);
|
|
586
|
+
return parseGitRemoteUrl(stdout.trim());
|
|
587
|
+
}
|
|
588
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
589
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(\.git)?$/);
|
|
590
|
+
if (sshMatch) {
|
|
591
|
+
const [, owner, repo] = sshMatch;
|
|
592
|
+
return {
|
|
593
|
+
owner,
|
|
594
|
+
repo: repo.replace(/\.git$/, ""),
|
|
595
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/([^/]+)\/(.+?)(\.git)?$/);
|
|
599
|
+
if (httpsMatch) {
|
|
600
|
+
const [, owner, repo] = httpsMatch;
|
|
601
|
+
return {
|
|
602
|
+
owner,
|
|
603
|
+
repo: repo.replace(/\.git$/, ""),
|
|
604
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
throw new Error(`Unable to parse git remote URL: ${remoteUrl}`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/shared/ui/interactive.ts
|
|
611
|
+
import * as p from "@clack/prompts";
|
|
612
|
+
|
|
613
|
+
// src/shared/ui/render/init.ts
|
|
614
|
+
function renderInitEvent(event, ctx) {
|
|
615
|
+
switch (event.type) {
|
|
616
|
+
case "init:start":
|
|
617
|
+
ctx.info(`Initializing fleet for ${event.owner}/${event.repo}`);
|
|
618
|
+
break;
|
|
619
|
+
case "init:branch:creating":
|
|
620
|
+
ctx.startSpinner(`Creating branch ${event.name} from ${event.base}`);
|
|
621
|
+
break;
|
|
622
|
+
case "init:branch:created":
|
|
623
|
+
ctx.stopSpinner(`Branch ${event.name} created`);
|
|
624
|
+
break;
|
|
625
|
+
case "init:file:committed":
|
|
626
|
+
ctx.info(` ✓ ${event.path}`);
|
|
627
|
+
break;
|
|
628
|
+
case "init:file:skipped":
|
|
629
|
+
ctx.warn(` ⊘ ${event.path} — ${event.reason}`);
|
|
630
|
+
break;
|
|
631
|
+
case "init:pr:creating":
|
|
632
|
+
ctx.startSpinner("Creating pull request…");
|
|
633
|
+
break;
|
|
634
|
+
case "init:pr:created":
|
|
635
|
+
ctx.stopSpinner(`PR #${event.number} created`);
|
|
636
|
+
ctx.info(` ${event.url}`);
|
|
637
|
+
break;
|
|
638
|
+
case "init:done":
|
|
639
|
+
ctx.success(`Fleet initialized — PR: ${event.prUrl}`);
|
|
640
|
+
break;
|
|
641
|
+
case "init:auth:detected":
|
|
642
|
+
ctx.success(`Auth: ${event.method === "token" ? "GITHUB_TOKEN" : "GitHub App"}`);
|
|
643
|
+
break;
|
|
644
|
+
case "init:secret:uploading":
|
|
645
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
646
|
+
break;
|
|
647
|
+
case "init:secret:uploaded":
|
|
648
|
+
ctx.stopSpinner(`Secret ${event.name} saved`);
|
|
649
|
+
break;
|
|
650
|
+
case "init:secret:skipped":
|
|
651
|
+
ctx.warn(` ⊘ ${event.name} — ${event.reason}`);
|
|
652
|
+
break;
|
|
653
|
+
case "init:dry-run":
|
|
654
|
+
ctx.info("Would create:");
|
|
655
|
+
event.files.forEach((f) => ctx.message(` ${f}`));
|
|
656
|
+
break;
|
|
657
|
+
case "init:already-initialized":
|
|
658
|
+
ctx.warn("Repository is already initialized");
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/shared/ui/render/configure.ts
|
|
664
|
+
function renderConfigureEvent(event, ctx) {
|
|
665
|
+
switch (event.type) {
|
|
666
|
+
case "configure:start":
|
|
667
|
+
ctx.info(`Configuring ${event.resource} for ${event.owner}/${event.repo}`);
|
|
668
|
+
break;
|
|
669
|
+
case "configure:label:created":
|
|
670
|
+
ctx.info(` ✓ Label "${event.name}" created`);
|
|
671
|
+
break;
|
|
672
|
+
case "configure:label:exists":
|
|
673
|
+
ctx.warn(` ⊘ Label "${event.name}" already exists`);
|
|
674
|
+
break;
|
|
675
|
+
case "configure:secret:uploading":
|
|
676
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
677
|
+
break;
|
|
678
|
+
case "configure:secret:uploaded":
|
|
679
|
+
ctx.stopSpinner(`Secret ${event.name} uploaded`);
|
|
680
|
+
break;
|
|
681
|
+
case "configure:done":
|
|
682
|
+
ctx.success("Configuration complete");
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/shared/ui/session-url.ts
|
|
688
|
+
var JULES_BASE_URL = "https://jules.google.com";
|
|
689
|
+
function sessionUrl(sessionId) {
|
|
690
|
+
return `${JULES_BASE_URL}/sessions/${sessionId}`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/shared/ui/render/analyze.ts
|
|
694
|
+
function renderAnalyzeEvent(event, ctx) {
|
|
695
|
+
switch (event.type) {
|
|
696
|
+
case "analyze:start":
|
|
697
|
+
ctx.info(`Analyzing ${event.goalCount} goal(s) for ${event.owner}/${event.repo}`);
|
|
698
|
+
break;
|
|
699
|
+
case "analyze:goal:start":
|
|
700
|
+
if (event.total > 1) {
|
|
701
|
+
ctx.step(`[${event.index}/${event.total}] ${event.file}`);
|
|
702
|
+
} else {
|
|
703
|
+
ctx.step(event.file);
|
|
704
|
+
}
|
|
705
|
+
if (event.milestone)
|
|
706
|
+
ctx.info(` Milestone: ${event.milestone}`);
|
|
707
|
+
break;
|
|
708
|
+
case "analyze:milestone:resolved":
|
|
709
|
+
ctx.info(` Milestone "${event.title}" (#${event.id})`);
|
|
710
|
+
break;
|
|
711
|
+
case "analyze:context:fetched":
|
|
712
|
+
ctx.info(` Context: ${event.openIssues} open, ${event.closedIssues} closed, ${event.prs} PRs`);
|
|
713
|
+
break;
|
|
714
|
+
case "analyze:session:dispatching":
|
|
715
|
+
ctx.startSpinner(`Dispatching session for ${event.goal}…`);
|
|
716
|
+
break;
|
|
717
|
+
case "analyze:session:started":
|
|
718
|
+
ctx.stopSpinner(`Session started: ${event.id}`);
|
|
719
|
+
ctx.info(` ${sessionUrl(event.id)}`);
|
|
720
|
+
break;
|
|
721
|
+
case "analyze:session:failed":
|
|
722
|
+
ctx.stopSpinner();
|
|
723
|
+
ctx.error(` Failed: ${event.error}`);
|
|
724
|
+
break;
|
|
725
|
+
case "analyze:done":
|
|
726
|
+
ctx.success(`Analysis complete — ${event.sessionsStarted} session(s) from ${event.goalsProcessed} goal(s)`);
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/shared/ui/render/dispatch.ts
|
|
732
|
+
function renderDispatchEvent(event, ctx) {
|
|
733
|
+
switch (event.type) {
|
|
734
|
+
case "dispatch:start":
|
|
735
|
+
ctx.info(`Dispatching from milestone ${event.milestone}`);
|
|
736
|
+
break;
|
|
737
|
+
case "dispatch:scanning":
|
|
738
|
+
ctx.startSpinner("Scanning for fleet issues…");
|
|
739
|
+
break;
|
|
740
|
+
case "dispatch:found":
|
|
741
|
+
ctx.stopSpinner(`Found ${event.count} undispatched issue(s)`);
|
|
742
|
+
break;
|
|
743
|
+
case "dispatch:issue:dispatching":
|
|
744
|
+
ctx.startSpinner(`#${event.number}: ${event.title}`);
|
|
745
|
+
break;
|
|
746
|
+
case "dispatch:issue:dispatched":
|
|
747
|
+
ctx.stopSpinner(`#${event.number} → session ${event.sessionId}`);
|
|
748
|
+
ctx.info(` ${sessionUrl(event.sessionId)}`);
|
|
749
|
+
break;
|
|
750
|
+
case "dispatch:issue:skipped":
|
|
751
|
+
ctx.warn(` ⊘ #${event.number}: ${event.reason}`);
|
|
752
|
+
break;
|
|
753
|
+
case "dispatch:done":
|
|
754
|
+
ctx.success(`Dispatch complete — ${event.dispatched} dispatched, ${event.skipped} skipped`);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/shared/ui/render/merge.ts
|
|
760
|
+
function renderMergeEvent(event, ctx) {
|
|
761
|
+
switch (event.type) {
|
|
762
|
+
case "merge:start":
|
|
763
|
+
ctx.info(`Merging ${event.prCount} PR(s) in ${event.owner}/${event.repo} [${event.mode}]`);
|
|
764
|
+
break;
|
|
765
|
+
case "merge:no-prs":
|
|
766
|
+
ctx.info("No PRs ready to merge.");
|
|
767
|
+
break;
|
|
768
|
+
case "merge:pr:processing":
|
|
769
|
+
ctx.startSpinner(`PR #${event.number}: ${event.title}${event.retry ? ` (retry ${event.retry})` : ""}`);
|
|
770
|
+
break;
|
|
771
|
+
case "merge:branch:updating":
|
|
772
|
+
ctx.startSpinner(`Updating branch for PR #${event.prNumber}…`);
|
|
773
|
+
break;
|
|
774
|
+
case "merge:branch:updated":
|
|
775
|
+
ctx.stopSpinner(`Branch updated for PR #${event.prNumber}`);
|
|
776
|
+
break;
|
|
777
|
+
case "merge:ci:waiting":
|
|
778
|
+
ctx.startSpinner(`Waiting for CI on PR #${event.prNumber}…`);
|
|
779
|
+
break;
|
|
780
|
+
case "merge:ci:check": {
|
|
781
|
+
const icon = event.status === "pass" ? "✓" : event.status === "fail" ? "✗" : "…";
|
|
782
|
+
const dur = event.duration ? ` (${event.duration}s)` : "";
|
|
783
|
+
ctx.info(` ${icon} ${event.name}${dur}`);
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case "merge:ci:passed":
|
|
787
|
+
ctx.stopSpinner(`CI passed for PR #${event.prNumber}`);
|
|
788
|
+
break;
|
|
789
|
+
case "merge:ci:failed":
|
|
790
|
+
ctx.stopSpinner(`CI failed for PR #${event.prNumber}`);
|
|
791
|
+
break;
|
|
792
|
+
case "merge:ci:timeout":
|
|
793
|
+
ctx.stopSpinner(`CI timed out for PR #${event.prNumber}`);
|
|
794
|
+
break;
|
|
795
|
+
case "merge:ci:none":
|
|
796
|
+
ctx.stopSpinner(`No CI checks for PR #${event.prNumber}`);
|
|
797
|
+
break;
|
|
798
|
+
case "merge:pr:merging":
|
|
799
|
+
ctx.startSpinner(`Merging PR #${event.prNumber}…`);
|
|
800
|
+
break;
|
|
801
|
+
case "merge:pr:merged":
|
|
802
|
+
ctx.stopSpinner(`PR #${event.prNumber} merged ✓`);
|
|
803
|
+
break;
|
|
804
|
+
case "merge:pr:skipped":
|
|
805
|
+
ctx.warn(` ⊘ PR #${event.prNumber}: ${event.reason}`);
|
|
806
|
+
break;
|
|
807
|
+
case "merge:conflict:detected":
|
|
808
|
+
ctx.stopSpinner(`Conflict detected on PR #${event.prNumber}`);
|
|
809
|
+
break;
|
|
810
|
+
case "merge:redispatch:start":
|
|
811
|
+
ctx.startSpinner(`Re-dispatching PR #${event.oldPr}…`);
|
|
812
|
+
break;
|
|
813
|
+
case "merge:redispatch:waiting":
|
|
814
|
+
ctx.startSpinner(`Waiting for re-dispatched PR (was #${event.oldPr})…`);
|
|
815
|
+
break;
|
|
816
|
+
case "merge:redispatch:done":
|
|
817
|
+
ctx.stopSpinner(`Re-dispatched: #${event.oldPr} → #${event.newPr}`);
|
|
818
|
+
break;
|
|
819
|
+
case "merge:done":
|
|
820
|
+
ctx.success(`Merge complete — ${event.merged.length} merged, ${event.skipped.length} skipped`);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/shared/ui/render/error.ts
|
|
826
|
+
function renderErrorEvent(event, ctx) {
|
|
827
|
+
ctx.stopSpinner();
|
|
828
|
+
ctx.error(`[${event.code}] ${event.message}`);
|
|
829
|
+
if (event.suggestion)
|
|
830
|
+
ctx.info(` \uD83D\uDCA1 ${event.suggestion}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/shared/ui/interactive.ts
|
|
834
|
+
class InteractiveRenderer {
|
|
835
|
+
spinner = null;
|
|
836
|
+
ctx = {
|
|
837
|
+
info: (msg) => p.log.info(msg),
|
|
838
|
+
success: (msg) => p.log.success(msg),
|
|
839
|
+
warn: (msg) => p.log.warn(msg),
|
|
840
|
+
error: (msg) => p.log.error(msg),
|
|
841
|
+
message: (msg) => p.log.message(msg),
|
|
842
|
+
step: (msg) => p.log.step(msg),
|
|
843
|
+
startSpinner: (msg) => this.startSpinner(msg),
|
|
844
|
+
stopSpinner: (msg) => this.stopSpinner(msg)
|
|
845
|
+
};
|
|
846
|
+
start(title) {
|
|
847
|
+
p.intro(title);
|
|
848
|
+
}
|
|
849
|
+
end(message) {
|
|
850
|
+
this.stopSpinner();
|
|
851
|
+
p.outro(message);
|
|
852
|
+
}
|
|
853
|
+
error(message) {
|
|
854
|
+
this.stopSpinner();
|
|
855
|
+
p.log.error(message);
|
|
856
|
+
}
|
|
857
|
+
render(event) {
|
|
858
|
+
if (event.type.startsWith("init:"))
|
|
859
|
+
return renderInitEvent(event, this.ctx);
|
|
860
|
+
if (event.type.startsWith("configure:"))
|
|
861
|
+
return renderConfigureEvent(event, this.ctx);
|
|
862
|
+
if (event.type.startsWith("analyze:"))
|
|
863
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
864
|
+
if (event.type.startsWith("dispatch:"))
|
|
865
|
+
return renderDispatchEvent(event, this.ctx);
|
|
866
|
+
if (event.type.startsWith("merge:"))
|
|
867
|
+
return renderMergeEvent(event, this.ctx);
|
|
868
|
+
if (event.type === "error")
|
|
869
|
+
return renderErrorEvent(event, this.ctx);
|
|
870
|
+
}
|
|
871
|
+
startSpinner(message) {
|
|
872
|
+
this.stopSpinner();
|
|
873
|
+
this.spinner = p.spinner();
|
|
874
|
+
this.spinner.start(message);
|
|
875
|
+
}
|
|
876
|
+
stopSpinner(message) {
|
|
877
|
+
if (this.spinner) {
|
|
878
|
+
this.spinner.stop(message);
|
|
879
|
+
this.spinner = null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/shared/ui/plain.ts
|
|
885
|
+
class PlainRenderer {
|
|
886
|
+
ctx = {
|
|
887
|
+
info: (msg) => console.log(msg),
|
|
888
|
+
success: (msg) => console.log(msg),
|
|
889
|
+
warn: (msg) => console.log(msg),
|
|
890
|
+
error: (msg) => console.error(msg),
|
|
891
|
+
message: (msg) => console.log(msg),
|
|
892
|
+
step: (msg) => console.log(msg),
|
|
893
|
+
startSpinner: (msg) => console.log(msg),
|
|
894
|
+
stopSpinner: (msg) => {
|
|
895
|
+
if (msg)
|
|
896
|
+
console.log(` ✓ ${msg}`);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
start(title) {
|
|
900
|
+
console.log(`
|
|
901
|
+
═══ ${title} ═══
|
|
902
|
+
`);
|
|
903
|
+
}
|
|
904
|
+
end(message) {
|
|
905
|
+
console.log(`
|
|
906
|
+
═══ ${message} ═══
|
|
907
|
+
`);
|
|
908
|
+
}
|
|
909
|
+
error(message) {
|
|
910
|
+
console.error(`ERROR: ${message}`);
|
|
911
|
+
}
|
|
912
|
+
render(event) {
|
|
913
|
+
if (event.type.startsWith("init:"))
|
|
914
|
+
return renderInitEvent(event, this.ctx);
|
|
915
|
+
if (event.type.startsWith("configure:"))
|
|
916
|
+
return renderConfigureEvent(event, this.ctx);
|
|
917
|
+
if (event.type.startsWith("analyze:"))
|
|
918
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
919
|
+
if (event.type.startsWith("dispatch:"))
|
|
920
|
+
return renderDispatchEvent(event, this.ctx);
|
|
921
|
+
if (event.type.startsWith("merge:"))
|
|
922
|
+
return renderMergeEvent(event, this.ctx);
|
|
923
|
+
if (event.type === "error")
|
|
924
|
+
return renderErrorEvent(event, this.ctx);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/shared/ui/index.ts
|
|
929
|
+
function isInteractive() {
|
|
930
|
+
if (process.env.CI === "true")
|
|
931
|
+
return false;
|
|
932
|
+
if (!process.stdout.isTTY)
|
|
933
|
+
return false;
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
function createRenderer(interactive) {
|
|
937
|
+
const useInteractive = interactive ?? isInteractive();
|
|
938
|
+
return useInteractive ? new InteractiveRenderer : new PlainRenderer;
|
|
939
|
+
}
|
|
940
|
+
function createEmitter(renderer) {
|
|
941
|
+
return (event) => renderer.render(event);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/cli/analyze.command.ts
|
|
945
|
+
var analyze_command_default = defineCommand({
|
|
946
|
+
meta: {
|
|
947
|
+
name: "analyze",
|
|
948
|
+
description: "Read goal file(s), fetch milestone context, and fire Jules analyzer sessions"
|
|
949
|
+
},
|
|
950
|
+
args: {
|
|
951
|
+
goal: {
|
|
952
|
+
type: "string",
|
|
953
|
+
description: "Path to a specific goal file"
|
|
954
|
+
},
|
|
955
|
+
"goals-dir": {
|
|
956
|
+
type: "string",
|
|
957
|
+
default: ".fleet/goals",
|
|
958
|
+
description: "Directory to auto-discover goal files from"
|
|
959
|
+
},
|
|
960
|
+
milestone: {
|
|
961
|
+
type: "string",
|
|
962
|
+
description: "Milestone ID to scope context"
|
|
963
|
+
},
|
|
964
|
+
owner: {
|
|
965
|
+
type: "string",
|
|
966
|
+
description: "Repository owner (auto-detected from git remote if omitted)"
|
|
967
|
+
},
|
|
968
|
+
repo: {
|
|
969
|
+
type: "string",
|
|
970
|
+
description: "Repository name (auto-detected from git remote if omitted)"
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
async run({ args }) {
|
|
974
|
+
const renderer = createRenderer();
|
|
975
|
+
let owner = args.owner;
|
|
976
|
+
let repo = args.repo;
|
|
977
|
+
if (!owner || !repo) {
|
|
978
|
+
const repoInfo = await getGitRepoInfo();
|
|
979
|
+
owner = owner || repoInfo.owner;
|
|
980
|
+
repo = repo || repoInfo.repo;
|
|
981
|
+
}
|
|
982
|
+
renderer.start(`Fleet Analyze — ${owner}/${repo}`);
|
|
983
|
+
const input = AnalyzeInputSchema.parse({
|
|
984
|
+
goal: args.goal || undefined,
|
|
985
|
+
goalsDir: args["goals-dir"],
|
|
986
|
+
milestone: args.milestone || undefined,
|
|
987
|
+
owner,
|
|
988
|
+
repo,
|
|
989
|
+
baseBranch: process.env.FLEET_BASE_BRANCH || "main"
|
|
990
|
+
});
|
|
991
|
+
const { jules } = await import("@google/jules-sdk");
|
|
992
|
+
const dispatcher = {
|
|
993
|
+
async dispatch(options) {
|
|
994
|
+
return jules.session({
|
|
995
|
+
prompt: options.prompt,
|
|
996
|
+
source: options.source,
|
|
997
|
+
requireApproval: options.requireApproval,
|
|
998
|
+
autoPr: options.autoPr
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const octokit = createFleetOctokit();
|
|
1003
|
+
const emit = createEmitter(renderer);
|
|
1004
|
+
const handler = new AnalyzeHandler({ octokit, dispatcher, emit });
|
|
1005
|
+
const result = await handler.execute(input);
|
|
1006
|
+
if (!result.success) {
|
|
1007
|
+
renderer.error(result.error.message);
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
renderer.end(`${result.data.sessionsStarted.length} session(s) dispatched.`);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
export {
|
|
1014
|
+
analyze_command_default as default
|
|
1015
|
+
};
|