Skip to content

Commit 630ed71

Browse files
upgrade to flue 0.3 (#16441)
* upgrade to flue 0.1.3 * upgrade to flue 0.3 * fix lint: update knip config and migrate merge-fix workflow to flue 0.3 API --------- Co-authored-by: Matthew Phillips <matthew@matthewphillips.info>
1 parent 3740b24 commit 630ed71

9 files changed

Lines changed: 2231 additions & 280 deletions

File tree

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,35 @@
1-
import type { FlueClient } from '@flue/client';
2-
import { anthropic, github, githubBody } from '@flue/client/proxies';
1+
import type { FlueContext, FlueSession } from '@flue/sdk/client';
2+
import { defineCommand } from '@flue/sdk/node';
33
import * as v from 'valibot';
44
import {
5+
GITHUB_TOKEN_BASE,
56
type IssueDetails,
67
type RepoLabel,
78
addGitHubLabels,
89
fetchIssueDetails,
910
fetchRepoLabels,
1011
postGitHubComment,
1112
removeGitHubLabel,
12-
} from './github.ts';
13-
14-
export const proxies = {
15-
anthropic: anthropic(),
16-
github: github({
17-
policy: {
18-
base: 'allow-read',
19-
allow: [
20-
// Allow read-only access to the GraphQL endpoint
21-
{ method: 'POST', path: '/graphql', body: githubBody.graphql() },
22-
// Allow git clone, fetch, and push over smart HTTP transport
23-
{ method: 'GET', path: '/*/*/info/refs' },
24-
{ method: 'POST', path: '/*/*/git-upload-pack' },
25-
{ method: 'POST', path: '/*/*/git-receive-pack' },
26-
],
27-
},
28-
}),
29-
};
13+
} from '../lib/github.ts';
14+
15+
// CLI-only agent: no HTTP trigger. Invoked from GitHub Actions via `flue run issue-triage`.
16+
export const triggers = {};
17+
18+
// Define commands that are allowed as pass-through to the local GH Actions container.
19+
const bgproc = defineCommand('bgproc');
20+
const agentBrowser = defineCommand('agent-browser');
21+
const node = defineCommand('node');
22+
const pnpm = defineCommand('pnpm');
23+
const gh = defineCommand('gh', { env: { GH_TOKEN: GITHUB_TOKEN_BASE } });
24+
const git = defineCommand('git');
25+
const gitWithAuth = defineCommand('git', { env: { GH_TOKEN: GITHUB_TOKEN_BASE } });
3026

3127
function assert(condition: unknown, message: string): asserts condition {
3228
if (!condition) throw new Error(message);
3329
}
3430

35-
async function shouldRetriage(flue: FlueClient, issue: IssueDetails): Promise<'yes' | 'no'> {
36-
return flue.prompt(
31+
async function shouldRetriage(session: FlueSession, issue: IssueDetails): Promise<'yes' | 'no'> {
32+
return session.prompt(
3733
`You are reviewing a GitHub issue conversation to decide whether a triage re-run is warranted.
3834
3935
## Issue
@@ -65,7 +61,7 @@ Return only "yes" or "no" inside the ---RESULT_START--- / ---RESULT_END--- block
6561
}
6662

6763
async function selectTriageLabels(
68-
flue: FlueClient,
64+
session: FlueSession,
6965
{
7066
comment,
7167
priorityLabels,
@@ -75,7 +71,7 @@ async function selectTriageLabels(
7571
const priorityLabelNames = priorityLabels.map((l) => l.name);
7672
const packageLabelNames = packageLabels.map((l) => l.name);
7773

78-
const labelResult = await flue.prompt(
74+
const labelResult = await session.prompt(
7975
`Label the following GitHub issue based on the triage report that was already posted.
8076
8177
Select labels for this issue from the lists below based on the triage report. Select exactly one priority label (the report's **Priority** section is a strong hint) and 0-3 package labels based on where the issue lives in the monorepo and how it manifests.
@@ -114,7 +110,7 @@ ${comment}
114110
}
115111

116112
async function runTriagePipeline(
117-
flue: FlueClient,
113+
session: FlueSession,
118114
issueNumber: number,
119115
issueDetails: IssueDetails,
120116
): Promise<{
@@ -127,8 +123,9 @@ async function runTriagePipeline(
127123
fixed: boolean;
128124
commitMessage: string | null;
129125
}> {
130-
const reproduceResult = await flue.skill('triage/reproduce.md', {
126+
const reproduceResult = await session.skill('triage/reproduce.md', {
131127
args: { issueNumber, issueDetails },
128+
commands: [gh, bgproc, agentBrowser, git, node, pnpm],
132129
result: v.object({
133130
reproducible: v.pipe(
134131
v.boolean(),
@@ -155,17 +152,19 @@ async function runTriagePipeline(
155152
};
156153
}
157154

158-
const diagnoseResult = await flue.skill('triage/diagnose.md', {
155+
const diagnoseResult = await session.skill('triage/diagnose.md', {
159156
args: { issueDetails },
157+
commands: [gh, bgproc, agentBrowser, git, node, pnpm],
160158
result: v.object({
161159
confidence: v.pipe(
162160
v.nullable(v.picklist(['high', 'medium', 'low'])),
163161
v.description('Diagnosis confidence level, null if not attempted'),
164162
),
165163
}),
166164
});
167-
const verifyResult = await flue.skill('triage/verify.md', {
165+
const verifyResult = await session.skill('triage/verify.md', {
168166
args: { issueDetails },
167+
commands: [gh, bgproc, agentBrowser, git, node, pnpm],
169168
result: v.object({
170169
verdict: v.pipe(
171170
v.picklist(['bug', 'intended-behavior', 'unclear']),
@@ -190,8 +189,9 @@ async function runTriagePipeline(
190189
};
191190
}
192191

193-
const fixResult = await flue.skill('triage/fix.md', {
192+
const fixResult = await session.skill('triage/fix.md', {
194193
args: { issueDetails },
194+
commands: [gh, bgproc, agentBrowser, git, node, pnpm],
195195
result: v.object({
196196
fixed: v.pipe(
197197
v.boolean(),
@@ -216,29 +216,31 @@ async function runTriagePipeline(
216216
};
217217
}
218218

219-
export const args = v.object({
220-
issueNumber: v.number(),
221-
});
222-
223-
export default async function triage(
224-
flue: FlueClient,
225-
{ issueNumber }: v.InferOutput<typeof args>,
226-
) {
219+
export default async function ({ init, payload }: FlueContext) {
220+
const issueNumber = payload.issueNumber as number;
227221
const branch = `flue/fix-${issueNumber}`;
222+
223+
// Initialize the agent and session.
224+
const agent = await init({
225+
sandbox: 'local',
226+
model: 'anthropic/claude-opus-4-6',
227+
});
228+
const session = await agent.session();
229+
228230
const issueDetails = await fetchIssueDetails(issueNumber);
229231

230232
// If there are prior comments, this is a re-triage. Check whether new
231233
// actionable information has been provided before running the full pipeline.
232234
const hasExistingConversation = issueDetails.comments.length > 0;
233235
if (hasExistingConversation) {
234-
const shouldRetriageResult = await shouldRetriage(flue, issueDetails);
236+
const shouldRetriageResult = await shouldRetriage(session, issueDetails);
235237
if (shouldRetriageResult === 'no') {
236238
return { skipped: true, reason: 'No new actionable information' };
237239
}
238240
}
239241

240242
// Run the triage pipeline: reproduce → diagnose → verify → fix
241-
const triageResult = await runTriagePipeline(flue, issueNumber, issueDetails);
243+
const triageResult = await runTriagePipeline(session, issueNumber, issueDetails);
242244
let isPushed = false;
243245

244246
// Push the fix branch if there are meaningful changes (fix, failing test, etc.).
@@ -247,19 +249,20 @@ export default async function triage(
247249
// - create a PR from that branch entirely in the GH UI
248250
// - ignore it completely
249251
{
250-
const diff = await flue.shell('git diff main --stat');
252+
const diff = await session.shell('git diff main --stat', { commands: [git] });
251253
if (diff.stdout.trim()) {
252-
const status = await flue.shell('git status --porcelain');
254+
const status = await session.shell('git status --porcelain', { commands: [git] });
253255
if (status.stdout.trim()) {
254-
await flue.shell('git add -A');
256+
await session.shell('git add -A', { commands: [git] });
255257
const defaultMessage = triageResult.fixed
256258
? 'fix(auto-triage): automated fix'
257259
: 'test(auto-triage): failing test and investigation notes';
258-
await flue.shell(
260+
await session.shell(
259261
`git commit -m ${JSON.stringify(triageResult.commitMessage ?? defaultMessage)}`,
262+
{ commands: [git] },
260263
);
261264
}
262-
const pushResult = await flue.shell(`git push -f origin ${branch}`);
265+
const pushResult = await session.shell(`git push -f origin ${branch}`, { commands: [gitWithAuth] });
263266
console.info('push result:', pushResult);
264267
isPushed = pushResult.exitCode === 0;
265268
}
@@ -272,8 +275,9 @@ export default async function triage(
272275
assert(packageLabels.length > 0, 'no package labels found');
273276

274277
const branchName = isPushed ? branch : null;
275-
const comment = await flue.skill('triage/comment.md', {
278+
const comment = await session.skill('triage/comment.md', {
276279
args: { branchName, priorityLabels, issueDetails },
280+
commands: [gh, git, node, pnpm],
277281
result: v.pipe(
278282
v.string(),
279283
v.description(
@@ -286,7 +290,7 @@ export default async function triage(
286290

287291
if (triageResult.reproducible) {
288292
await removeGitHubLabel(issueNumber, 'needs triage');
289-
const selectedLabels = await selectTriageLabels(flue, {
293+
const selectedLabels = await selectTriageLabels(session, {
290294
comment,
291295
priorityLabels,
292296
packageLabels,
Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import * as v from 'valibot';
22

3-
const REPO = 'withastro/astro';
3+
const REPO = process.env.GITHUB_REPOSITORY || 'withastro/astro';
4+
export const GITHUB_TOKEN_BASE = process.env.GITHUB_TOKEN;
45

5-
function headers(): Record<string, string> {
6-
const token = process.env.FREDKBOT_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
7-
if (!token) throw new Error('token is not set');
6+
// Intentionally not exported, GITHUB_TOKEN_BASE should be enough anywhere else.
7+
const GITHUB_TOKEN_PRIVILEGED = process.env.FREDKBOT_GITHUB_TOKEN;
8+
9+
function assert(condition: unknown, message: string): asserts condition {
10+
if (!condition) throw new Error(message);
11+
}
12+
13+
function headers(token: string): Record<string, string> {
814
return {
915
Authorization: `token ${token}`,
1016
'Content-Type': 'application/json',
@@ -40,10 +46,13 @@ export const repoLabelSchema = v.object({
4046
export type RepoLabel = v.InferOutput<typeof repoLabelSchema>;
4147

4248
export async function fetchIssueDetails(issueNumber: number): Promise<IssueDetails> {
49+
assert(GITHUB_TOKEN_BASE, `GITHUB_TOKEN env token is required.`);
4350
const [issueRes, commentsRes] = await Promise.all([
44-
fetch(`https://api.github.com/repos/${REPO}/issues/${issueNumber}`, { headers: headers() }),
51+
fetch(`https://api.github.com/repos/${REPO}/issues/${issueNumber}`, {
52+
headers: headers(GITHUB_TOKEN_BASE),
53+
}),
4554
fetch(`https://api.github.com/repos/${REPO}/issues/${issueNumber}/comments?per_page=100`, {
46-
headers: headers(),
55+
headers: headers(GITHUB_TOKEN_BASE),
4756
}),
4857
]);
4958

@@ -84,14 +93,15 @@ export async function fetchRepoLabels(): Promise<{
8493
priorityLabels: RepoLabel[];
8594
packageLabels: RepoLabel[];
8695
}> {
96+
assert(GITHUB_TOKEN_BASE, `GITHUB_TOKEN env token is required.`);
8797
const allLabels: RepoLabel[] = [];
8898
let page = 1;
8999

90100
// Paginate through all labels (100 per page)
91101
while (true) {
92102
const res = await fetch(
93103
`https://api.github.com/repos/${REPO}/labels?per_page=100&page=${page}`,
94-
{ headers: headers() },
104+
{ headers: headers(GITHUB_TOKEN_BASE) },
95105
);
96106
if (!res.ok) {
97107
throw new Error(`Failed to fetch labels (HTTP ${res.status}): ${await res.text()}`);
@@ -109,9 +119,10 @@ export async function fetchRepoLabels(): Promise<{
109119
}
110120

111121
export async function postGitHubComment(issueNumber: number, body: string): Promise<void> {
122+
assert(GITHUB_TOKEN_PRIVILEGED, `FREDKBOT_GITHUB_TOKEN token is required.`);
112123
const res = await fetch(`https://api.github.com/repos/${REPO}/issues/${issueNumber}/comments`, {
113124
method: 'POST',
114-
headers: headers(),
125+
headers: headers(GITHUB_TOKEN_PRIVILEGED),
115126
body: JSON.stringify({ body }),
116127
});
117128
if (!res.ok) {
@@ -120,9 +131,10 @@ export async function postGitHubComment(issueNumber: number, body: string): Prom
120131
}
121132

122133
export async function addGitHubLabels(issueNumber: number, labels: string[]): Promise<void> {
134+
assert(GITHUB_TOKEN_PRIVILEGED, `FREDKBOT_GITHUB_TOKEN token is required.`);
123135
const res = await fetch(`https://api.github.com/repos/${REPO}/issues/${issueNumber}/labels`, {
124136
method: 'POST',
125-
headers: headers(),
137+
headers: headers(GITHUB_TOKEN_PRIVILEGED),
126138
body: JSON.stringify({ labels }),
127139
});
128140
if (!res.ok) {
@@ -131,11 +143,12 @@ export async function addGitHubLabels(issueNumber: number, labels: string[]): Pr
131143
}
132144

133145
export async function removeGitHubLabel(issueNumber: number, label: string): Promise<void> {
146+
assert(GITHUB_TOKEN_PRIVILEGED, `FREDKBOT_GITHUB_TOKEN token is required.`);
134147
const res = await fetch(
135148
`https://api.github.com/repos/${REPO}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`,
136149
{
137150
method: 'DELETE',
138-
headers: headers(),
151+
headers: headers(GITHUB_TOKEN_PRIVILEGED),
139152
},
140153
);
141154
if (!res.ok && res.status !== 404) {

.flue/sandbox/AGENTS.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

.flue/sandbox/Dockerfile

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)