@flumecode/runner 0.10.0 → 0.12.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/dist/cli.js +1082 -870
- package/package.json +1 -1
- package/skills-plugin/skills/implement-plan/SKILL.md +10 -8
- package/skills-plugin/skills/lint-plugin-generator/SKILL.md +96 -0
- package/skills-plugin/skills/request-to-plan/SKILL.md +13 -5
- package/skills-plugin/skills/resolve-merge-conflict/SKILL.md +32 -4
package/dist/cli.js
CHANGED
|
@@ -149,6 +149,25 @@ async function uploadJobLog(config, jobId, content) {
|
|
|
149
149
|
noteServerVersion(res);
|
|
150
150
|
if (!res.ok) throw new Error(`log upload failed: ${res.status} ${await safeText(res)}`);
|
|
151
151
|
}
|
|
152
|
+
async function fetchRelatedSessions(config, params) {
|
|
153
|
+
if (!params.prNumbers.length) return [];
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(`${config.serverUrl}/api/runner/sessions/related`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
authorization: `Bearer ${config.token}`,
|
|
159
|
+
"content-type": "application/json",
|
|
160
|
+
[RUNNER_VERSION_HEADER]: RUNNER_VERSION
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify(params)
|
|
163
|
+
});
|
|
164
|
+
noteServerVersion(res);
|
|
165
|
+
if (!res.ok) return [];
|
|
166
|
+
return await res.json();
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
152
171
|
async function safeText(res) {
|
|
153
172
|
try {
|
|
154
173
|
return await res.text();
|
|
@@ -157,930 +176,1106 @@ async function safeText(res) {
|
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
178
|
|
|
160
|
-
// src/
|
|
161
|
-
import {
|
|
162
|
-
import {
|
|
179
|
+
// src/plugins/socket.ts
|
|
180
|
+
import { exec as execCb } from "node:child_process";
|
|
181
|
+
import { promisify as promisify2 } from "node:util";
|
|
163
182
|
|
|
164
|
-
// src/
|
|
165
|
-
import {
|
|
166
|
-
import {
|
|
167
|
-
import {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
var optionsSchema = z.array(z.string().min(1)).min(2).max(8).describe("2\u20138 short, distinct choices for the user to pick from.");
|
|
176
|
-
var TAIL = "Do NOT add an 'Other' or 'None of these' catch-all \u2014 the UI always offers an 'Other' free-text option automatically. After calling this, END YOUR TURN and wait: the user's answer arrives as their next message and starts a fresh run.";
|
|
177
|
-
function createWidgetTooling() {
|
|
178
|
-
const collected = [];
|
|
179
|
-
const singleSelect = tool(
|
|
180
|
-
SINGLE_SELECT,
|
|
181
|
-
"Ask the user a single-select (radio-button) question \u2014 exactly one answer. Use this for a genuine either/or choice (competing approaches, scope decisions, yes/no) instead of writing the options as prose. " + TAIL,
|
|
182
|
-
{
|
|
183
|
-
question: z.string().min(1).describe("The question to ask the user."),
|
|
184
|
-
options: optionsSchema
|
|
185
|
-
},
|
|
186
|
-
async (args) => {
|
|
187
|
-
collected.push({
|
|
188
|
-
id: randomUUID(),
|
|
189
|
-
type: "single_select",
|
|
190
|
-
question: args.question,
|
|
191
|
-
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
192
|
-
selectedOptionId: null,
|
|
193
|
-
customAnswer: null
|
|
194
|
-
});
|
|
195
|
-
return widgetPosted("single-select");
|
|
196
|
-
}
|
|
197
|
-
);
|
|
198
|
-
const multiSelect = tool(
|
|
199
|
-
MULTI_SELECT,
|
|
200
|
-
"Ask the user a multi-select (checkbox) question \u2014 they may pick any number of options, including none of the presets if they use 'Other'. Use this for 'select all that apply' questions (which features to include, which files to touch). " + TAIL,
|
|
201
|
-
{
|
|
202
|
-
question: z.string().min(1).describe("The question to ask the user."),
|
|
203
|
-
options: optionsSchema
|
|
204
|
-
},
|
|
205
|
-
async (args) => {
|
|
206
|
-
collected.push({
|
|
207
|
-
id: randomUUID(),
|
|
208
|
-
type: "multi_select",
|
|
209
|
-
question: args.question,
|
|
210
|
-
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
211
|
-
selectedOptionIds: null,
|
|
212
|
-
customAnswer: null
|
|
213
|
-
});
|
|
214
|
-
return widgetPosted("multi-select");
|
|
215
|
-
}
|
|
216
|
-
);
|
|
217
|
-
const mcpServer = createSdkMcpServer({
|
|
218
|
-
name: SERVER_NAME,
|
|
219
|
-
tools: [singleSelect, multiSelect]
|
|
220
|
-
});
|
|
221
|
-
return { mcpServer, collected };
|
|
183
|
+
// src/workspace.ts
|
|
184
|
+
import { execFile } from "node:child_process";
|
|
185
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
186
|
+
import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
187
|
+
import { tmpdir } from "node:os";
|
|
188
|
+
import { join as join2 } from "node:path";
|
|
189
|
+
import { promisify } from "node:util";
|
|
190
|
+
|
|
191
|
+
// src/types.ts
|
|
192
|
+
function jobTitle(ctx) {
|
|
193
|
+
return ctx.kind === "init" ? "Initialize FlumeCode wiki" : ctx.request?.title ?? "request";
|
|
222
194
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
};
|
|
195
|
+
|
|
196
|
+
// src/logger.ts
|
|
197
|
+
var lines = [];
|
|
198
|
+
var secrets = [];
|
|
199
|
+
var MAX_BYTES = 10 * 1024 * 1024;
|
|
200
|
+
function startJobLog(opts) {
|
|
201
|
+
lines = [];
|
|
202
|
+
secrets = opts.secrets.filter(Boolean);
|
|
203
|
+
logEvent("meta", `job ${opts.jobId} (${opts.kind}) started at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
232
204
|
}
|
|
205
|
+
function redact(s) {
|
|
206
|
+
for (const sec of secrets) {
|
|
207
|
+
s = s.split(sec).join("***REDACTED***");
|
|
208
|
+
}
|
|
209
|
+
return s;
|
|
210
|
+
}
|
|
211
|
+
function logEvent(section, text) {
|
|
212
|
+
lines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${section}] ${redact(text)}`);
|
|
213
|
+
}
|
|
214
|
+
function getJobLog() {
|
|
215
|
+
const full = lines.join("\n");
|
|
216
|
+
if (full.length <= MAX_BYTES) return full;
|
|
217
|
+
const half = Math.floor(MAX_BYTES / 2);
|
|
218
|
+
return full.slice(0, half) + `
|
|
233
219
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
var
|
|
241
|
-
var
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
title: z2.string().min(1).max(120).describe(
|
|
254
|
-
"A concise, descriptive name for THIS plan. Must be distinct from the request title and from any sibling plans on the same request. Keep it under 120 characters."
|
|
255
|
-
),
|
|
256
|
-
scope: z2.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
|
|
257
|
-
goal: z2.string().min(1).describe("One or two sentences stating the outcome."),
|
|
258
|
-
assumptions: z2.array(z2.string()).describe("Anything decided during planning, including unanswered defaults."),
|
|
259
|
-
steps: z2.array(stepSchema).min(1).describe("Ordered list of changes. Each step says what and why, with file references."),
|
|
260
|
-
acceptanceCriteria: z2.array(z2.string().min(1)).min(2).describe(
|
|
261
|
-
"Observable conditions that together define done. Each must be checkable \u2014 a behavior, test, or state you could verify \u2014 not a restatement of a step. At least 2 required."
|
|
262
|
-
),
|
|
263
|
-
risks: z2.array(z2.string()).describe("Anything that could change the approach."),
|
|
264
|
-
outOfScope: z2.array(z2.string()).describe("What is deliberately not being done.")
|
|
265
|
-
};
|
|
266
|
-
var planSchema = z2.object(planInputSchema);
|
|
267
|
-
function renderPlan(plan) {
|
|
268
|
-
const lines2 = [];
|
|
269
|
-
lines2.push(`# ${plan.title}`);
|
|
270
|
-
lines2.push("");
|
|
271
|
-
lines2.push(`**Scope** \u2014 \`${plan.scope}\``);
|
|
272
|
-
lines2.push("");
|
|
273
|
-
lines2.push(`**Goal** \u2014 ${plan.goal}`);
|
|
274
|
-
if (plan.assumptions.length > 0) {
|
|
275
|
-
lines2.push("");
|
|
276
|
-
lines2.push("**Assumptions**");
|
|
277
|
-
for (const assumption of plan.assumptions) {
|
|
278
|
-
lines2.push(`- ${assumption}`);
|
|
279
|
-
}
|
|
220
|
+
\u2026[truncated ${full.length - MAX_BYTES} bytes]\u2026
|
|
221
|
+
|
|
222
|
+
` + full.slice(-half);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/workspace.ts
|
|
226
|
+
var exec = promisify(execFile);
|
|
227
|
+
var WORKSPACE_PREFIX = "flume-runner-";
|
|
228
|
+
var MAX_BUFFER = 1 << 24;
|
|
229
|
+
async function git(args) {
|
|
230
|
+
logEvent("git", `git ${args.join(" ")}`);
|
|
231
|
+
try {
|
|
232
|
+
const result = await exec("git", args, { maxBuffer: MAX_BUFFER });
|
|
233
|
+
if (result.stdout.trim()) logEvent("git:out", result.stdout.trim());
|
|
234
|
+
if (result.stderr.trim()) logEvent("git:err", result.stderr.trim());
|
|
235
|
+
return result;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
logEvent("git:err", String(err.stderr ?? err));
|
|
238
|
+
throw err;
|
|
280
239
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
240
|
+
}
|
|
241
|
+
async function ensureGitIdentity(dir, identity) {
|
|
242
|
+
await git(["-C", dir, "config", "user.email", identity.email]);
|
|
243
|
+
await git(["-C", dir, "config", "user.name", identity.name]);
|
|
244
|
+
}
|
|
245
|
+
function cloneUrl(ctx) {
|
|
246
|
+
const { owner, name, cloneToken } = ctx.repo;
|
|
247
|
+
return `https://x-access-token:${cloneToken}@github.com/${owner}/${name}.git`;
|
|
248
|
+
}
|
|
249
|
+
function detectPackageManager(dir) {
|
|
250
|
+
if (!existsSync2(join2(dir, "package.json"))) return null;
|
|
251
|
+
if (existsSync2(join2(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
252
|
+
if (existsSync2(join2(dir, "yarn.lock"))) return "yarn";
|
|
253
|
+
if (existsSync2(join2(dir, "package-lock.json"))) return "npm";
|
|
254
|
+
if (existsSync2(join2(dir, "bun.lockb"))) return "bun";
|
|
255
|
+
return "npm";
|
|
256
|
+
}
|
|
257
|
+
async function installDependencies(dir) {
|
|
258
|
+
const manager = detectPackageManager(dir);
|
|
259
|
+
if (manager === null) return { status: "skipped" };
|
|
260
|
+
const env = { ...process.env, CI: "1", ADBLOCK: "1", DISABLE_OPENCOLLECTIVE: "1" };
|
|
261
|
+
logEvent("install", `${manager} install`);
|
|
262
|
+
try {
|
|
263
|
+
const result = await exec(manager, ["install"], {
|
|
264
|
+
cwd: dir,
|
|
265
|
+
maxBuffer: MAX_BUFFER,
|
|
266
|
+
env,
|
|
267
|
+
timeout: 5 * 6e4
|
|
268
|
+
});
|
|
269
|
+
if (result.stdout.trim()) logEvent("install:out", result.stdout.trim());
|
|
270
|
+
if (result.stderr.trim()) logEvent("install:err", result.stderr.trim());
|
|
271
|
+
return { status: "installed", manager };
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const e = err;
|
|
274
|
+
const detail = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean).join("\n");
|
|
275
|
+
logEvent("install:err", detail || (err instanceof Error ? err.message : String(err)));
|
|
276
|
+
return { status: "failed", manager, error: err instanceof Error ? err.message : String(err) };
|
|
298
277
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
278
|
+
}
|
|
279
|
+
async function makeWorkspace() {
|
|
280
|
+
return mkdtemp(join2(tmpdir(), WORKSPACE_PREFIX));
|
|
281
|
+
}
|
|
282
|
+
var MAX_WORKSPACES = 8;
|
|
283
|
+
var workspaceRegistry = /* @__PURE__ */ new Map();
|
|
284
|
+
async function acquireWorkspace(key) {
|
|
285
|
+
const existing = workspaceRegistry.get(key);
|
|
286
|
+
if (existing !== void 0 && existsSync2(existing)) {
|
|
287
|
+
workspaceRegistry.delete(key);
|
|
288
|
+
workspaceRegistry.set(key, existing);
|
|
289
|
+
return { dir: existing, reused: true };
|
|
303
290
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
291
|
+
const dir = await makeWorkspace();
|
|
292
|
+
workspaceRegistry.set(key, dir);
|
|
293
|
+
if (workspaceRegistry.size > MAX_WORKSPACES) {
|
|
294
|
+
const oldest = workspaceRegistry.keys().next().value;
|
|
295
|
+
const oldDir = workspaceRegistry.get(oldest);
|
|
296
|
+
workspaceRegistry.delete(oldest);
|
|
297
|
+
rm(oldDir, { recursive: true, force: true }).catch(() => {
|
|
298
|
+
});
|
|
310
299
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
300
|
+
return { dir, reused: false };
|
|
301
|
+
}
|
|
302
|
+
async function discardWorkspace(key) {
|
|
303
|
+
const dir = workspaceRegistry.get(key);
|
|
304
|
+
workspaceRegistry.delete(key);
|
|
305
|
+
if (dir !== void 0) {
|
|
306
|
+
await cleanup(dir).catch(() => {
|
|
307
|
+
});
|
|
317
308
|
}
|
|
318
|
-
lines2.push("");
|
|
319
|
-
lines2.push(PLAN_MARKER);
|
|
320
|
-
return lines2.join("\n");
|
|
321
309
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return new Set(titles).size === titles.length;
|
|
327
|
-
},
|
|
328
|
-
{ message: "Each plan must have a distinct non-empty title" }
|
|
329
|
-
)
|
|
330
|
-
};
|
|
331
|
-
var submitPlanSchema = z2.object(submitPlanInputSchema);
|
|
332
|
-
function createPlanTooling() {
|
|
333
|
-
let renderedPlans = null;
|
|
334
|
-
const submitPlan = tool2(
|
|
335
|
-
SUBMIT_PLAN,
|
|
336
|
-
"Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable Accept-as-plan draft. Do NOT call submit_plan more than once. acceptanceCriteria is required in each plan and must contain at least 2 observable, verifiable conditions. The 'title' field names each specific plan \u2014 make it concise and distinct from the request title and from sibling plan titles.",
|
|
337
|
-
submitPlanInputSchema,
|
|
338
|
-
async (args) => {
|
|
339
|
-
const parsed = submitPlanSchema.parse(args);
|
|
340
|
-
renderedPlans = parsed.plans.map(renderPlan);
|
|
341
|
-
return {
|
|
342
|
-
content: [
|
|
343
|
-
{
|
|
344
|
-
type: "text",
|
|
345
|
-
text: "Plan(s) submitted. The runner will render and post them as your comment(s). End your turn now."
|
|
346
|
-
}
|
|
347
|
-
]
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
);
|
|
351
|
-
const mcpServer = createSdkMcpServer2({
|
|
352
|
-
name: SERVER_NAME2,
|
|
353
|
-
tools: [submitPlan]
|
|
310
|
+
async function resetWorkspace(dir) {
|
|
311
|
+
await git(["-C", dir, "reset", "--hard", "HEAD"]).catch(() => {
|
|
312
|
+
});
|
|
313
|
+
await git(["-C", dir, "clean", "-fd"]).catch(() => {
|
|
354
314
|
});
|
|
355
|
-
return { mcpServer, getPlans: () => renderedPlans };
|
|
356
315
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
var REPORT_TOOL_NAME = `mcp__${SERVER_NAME3}__${SUBMIT_REPORT}`;
|
|
364
|
-
var STATUS_ICON = {
|
|
365
|
-
met: "\u2705",
|
|
366
|
-
not_met: "\u274C",
|
|
367
|
-
unclear: "\u26A0\uFE0F"
|
|
368
|
-
};
|
|
369
|
-
var evidenceSchema = z3.object({
|
|
370
|
-
file: z3.string().min(1).describe("Repo-relative path the hunk comes from."),
|
|
371
|
-
hunk: z3.string().min(1).describe(
|
|
372
|
-
"A unified-diff hunk body proving the criterion \u2014 the lines that matter, not the whole file. Rendered verbatim as a ```diff block."
|
|
373
|
-
),
|
|
374
|
-
note: z3.string().optional().describe("Optional one-line explanation of why this hunk satisfies the criterion.")
|
|
375
|
-
});
|
|
376
|
-
var acVerdictSchema = z3.object({
|
|
377
|
-
criterion: z3.string().min(1).describe("The acceptance-criterion text, verbatim from the plan."),
|
|
378
|
-
status: z3.enum(["met", "not_met", "unclear"]).describe("Verdict for this criterion, verified against the actual diff."),
|
|
379
|
-
rationale: z3.string().min(1).describe("One or two sentences on why the verdict holds."),
|
|
380
|
-
evidence: z3.array(evidenceSchema).describe(
|
|
381
|
-
"Diff hunks proving the verdict. Include the relevant hunk(s) for a met criterion; may be empty for not_met / unclear."
|
|
382
|
-
)
|
|
383
|
-
});
|
|
384
|
-
var reportInputSchema = {
|
|
385
|
-
summary: z3.string().min(1).describe("One or two sentences on what was implemented."),
|
|
386
|
-
prose: z3.string().min(1).describe(
|
|
387
|
-
"Markdown for the remaining report sections \u2014 What changed, Files changed, Build / tests, and Caveats / follow-ups. Use ## headings. Do NOT include the acceptance-criteria section here (that goes in acceptanceCriteria) and do NOT include the PR link (the runner appends it)."
|
|
388
|
-
),
|
|
389
|
-
acceptanceCriteria: z3.array(acVerdictSchema).min(1).describe(
|
|
390
|
-
"One entry per acceptance criterion from the plan, in plan order, each with a verdict and the diff evidence behind it."
|
|
391
|
-
)
|
|
392
|
-
};
|
|
393
|
-
var reportSchema = z3.object(reportInputSchema);
|
|
394
|
-
function renderReport(report) {
|
|
395
|
-
const lines2 = [];
|
|
396
|
-
lines2.push(report.summary.trim());
|
|
397
|
-
lines2.push("");
|
|
398
|
-
lines2.push(report.prose.trim());
|
|
399
|
-
lines2.push("");
|
|
400
|
-
lines2.push("## Acceptance criteria");
|
|
401
|
-
for (const ac of report.acceptanceCriteria) {
|
|
402
|
-
lines2.push("");
|
|
403
|
-
lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
|
|
404
|
-
lines2.push("");
|
|
405
|
-
lines2.push(ac.rationale.trim());
|
|
406
|
-
for (const ev of ac.evidence) {
|
|
407
|
-
lines2.push("");
|
|
408
|
-
lines2.push(ev.note ? `\`${ev.file}\` \u2014 ${ev.note}` : `\`${ev.file}\``);
|
|
409
|
-
lines2.push("");
|
|
410
|
-
lines2.push("```diff");
|
|
411
|
-
lines2.push(ev.hunk.replace(/\n+$/, ""));
|
|
412
|
-
lines2.push("```");
|
|
413
|
-
}
|
|
316
|
+
async function prepareAtSha(ctx, dir, reused) {
|
|
317
|
+
const identity = { name: ctx.agentName, email: ctx.agentEmail };
|
|
318
|
+
if (!reused) {
|
|
319
|
+
await cloneAtSha(ctx, dir);
|
|
320
|
+
await ensureGitIdentity(dir, identity);
|
|
321
|
+
return;
|
|
414
322
|
}
|
|
415
|
-
|
|
323
|
+
await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
|
|
324
|
+
await ensureGitIdentity(dir, identity);
|
|
416
325
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
326
|
+
async function prepareResumingBranch(ctx, dir, reused) {
|
|
327
|
+
const identity = { name: ctx.agentName, email: ctx.agentEmail };
|
|
328
|
+
if (!reused) {
|
|
329
|
+
const result = await cloneResumingBranch(ctx, dir);
|
|
330
|
+
await ensureGitIdentity(dir, identity);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
|
|
334
|
+
await ensureGitIdentity(dir, identity);
|
|
335
|
+
return { resumed: true };
|
|
336
|
+
}
|
|
337
|
+
async function sweepWorkspaces() {
|
|
338
|
+
const base = tmpdir();
|
|
339
|
+
let entries;
|
|
340
|
+
try {
|
|
341
|
+
entries = await readdir(base);
|
|
342
|
+
} catch {
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
let removed = 0;
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
if (!entry.startsWith(WORKSPACE_PREFIX)) continue;
|
|
348
|
+
try {
|
|
349
|
+
await rm(join2(base, entry), { recursive: true, force: true });
|
|
350
|
+
removed++;
|
|
351
|
+
} catch {
|
|
433
352
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
name: SERVER_NAME3,
|
|
437
|
-
tools: [submitReport]
|
|
438
|
-
});
|
|
439
|
-
return { mcpServer, getReport: () => submittedReport };
|
|
353
|
+
}
|
|
354
|
+
return removed;
|
|
440
355
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
var secrets = [];
|
|
445
|
-
var MAX_BYTES = 10 * 1024 * 1024;
|
|
446
|
-
function startJobLog(opts) {
|
|
447
|
-
lines = [];
|
|
448
|
-
secrets = opts.secrets.filter(Boolean);
|
|
449
|
-
logEvent("meta", `job ${opts.jobId} (${opts.kind}) started at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
356
|
+
async function cloneAtSha(ctx, dir) {
|
|
357
|
+
await git(["clone", "--quiet", cloneUrl(ctx), dir]);
|
|
358
|
+
await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
|
|
450
359
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
|
|
360
|
+
async function cloneResumingBranch(ctx, dir) {
|
|
361
|
+
await git(["clone", "--quiet", cloneUrl(ctx), dir]);
|
|
362
|
+
try {
|
|
363
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", ctx.repo.checkoutBranch]);
|
|
364
|
+
await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, "FETCH_HEAD"]);
|
|
365
|
+
return { resumed: true };
|
|
366
|
+
} catch {
|
|
367
|
+
await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
|
|
368
|
+
return { resumed: false };
|
|
454
369
|
}
|
|
455
|
-
return s;
|
|
456
370
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
371
|
+
async function hasChanges(dir) {
|
|
372
|
+
await git(["-C", dir, "add", "-A"]);
|
|
373
|
+
const { stdout: stdout2 } = await git(["-C", dir, "status", "--porcelain"]);
|
|
374
|
+
return stdout2.trim().length > 0;
|
|
459
375
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
376
|
+
var PreCommitError = class extends Error {
|
|
377
|
+
constructor(log) {
|
|
378
|
+
super("pre-commit checks failed");
|
|
379
|
+
this.log = log;
|
|
380
|
+
this.name = "PreCommitError";
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function commitFailureLog(err) {
|
|
384
|
+
const e = err;
|
|
385
|
+
const parts = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter((s) => s.length > 0);
|
|
386
|
+
return parts.length > 0 ? parts.join("\n") : e.message ?? String(err);
|
|
469
387
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
388
|
+
function isUnsupportedGitSubcommand(err) {
|
|
389
|
+
const e = err;
|
|
390
|
+
const text = `${typeof e.stderr === "string" ? e.stderr : ""}
|
|
391
|
+
${e.message ?? ""}`;
|
|
392
|
+
return /is not a git command|unknown subcommand|usage: git hook/i.test(text);
|
|
393
|
+
}
|
|
394
|
+
async function runRepoChecks(dir) {
|
|
395
|
+
try {
|
|
396
|
+
await git(["-C", dir, "hook", "run", "pre-commit"]);
|
|
397
|
+
logEvent("checks", "pre-commit hook passed");
|
|
398
|
+
return { ok: true, log: "", skipped: false };
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (isUnsupportedGitSubcommand(err)) {
|
|
401
|
+
logEvent("checks", "pre-commit hook skipped (git too old)");
|
|
402
|
+
return { ok: true, log: "", skipped: true };
|
|
403
|
+
}
|
|
404
|
+
const log = commitFailureLog(err);
|
|
405
|
+
logEvent("checks:err", log);
|
|
406
|
+
return { ok: false, log, skipped: false };
|
|
479
407
|
}
|
|
480
|
-
return JSON.stringify(content);
|
|
481
408
|
}
|
|
482
|
-
async function
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
prompt: opts.prompt,
|
|
489
|
-
options: {
|
|
490
|
-
cwd: opts.cwd,
|
|
491
|
-
permissionMode: opts.permissionMode,
|
|
492
|
-
allowDangerouslySkipPermissions: opts.permissionMode === "bypassPermissions",
|
|
493
|
-
...opts.model ? { model: opts.model } : {},
|
|
494
|
-
...opts.abortController ? { abortController: opts.abortController } : {},
|
|
495
|
-
maxTurns: opts.maxTurns ?? 40,
|
|
496
|
-
// Hermetic: ignore ALL config from the checked-out repo (CLAUDE.md,
|
|
497
|
-
// .claude/settings.json, .claude/skills, …) and the runner owner's
|
|
498
|
-
// ~/.claude. Only our plugin's skills are exposed to the agent. Auto-memory
|
|
499
|
-
// is disabled separately via CLAUDE_CODE_DISABLE_AUTO_MEMORY (see cli.ts).
|
|
500
|
-
settingSources: [],
|
|
501
|
-
plugins: [{ type: "local", path: FLUME_PLUGIN_DIR }],
|
|
502
|
-
// Pre-approve the widget, plan, and Task tools so they run without a
|
|
503
|
-
// permission prompt in headless mode (allow-listing pre-approves them; it
|
|
504
|
-
// does NOT restrict anything else). Task lets the implement-plan
|
|
505
|
-
// orchestrator spawn its subagents; without pre-approval the spawn could
|
|
506
|
-
// stall waiting for an approval no one can give.
|
|
507
|
-
mcpServers: { flume_widgets: mcpServer, flume_plan: planServer, flume_report: reportServer },
|
|
508
|
-
allowedTools: [...WIDGET_TOOL_NAMES, PLAN_TOOL_NAME, REPORT_TOOL_NAME, "Task"]
|
|
509
|
-
}
|
|
510
|
-
})) {
|
|
511
|
-
if (message.type === "assistant") {
|
|
512
|
-
const content = message.message?.content;
|
|
513
|
-
if (Array.isArray(content)) {
|
|
514
|
-
for (const block of content) {
|
|
515
|
-
if (block && block.type === "text" && typeof block.text === "string") {
|
|
516
|
-
process.stdout.write(block.text);
|
|
517
|
-
logEvent("agent", block.text);
|
|
518
|
-
} else if (block && block.type === "tool_use") {
|
|
519
|
-
logEvent("tool_use", `${block.name} ${JSON.stringify(block.input)}`);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
} else if (message.type === "user") {
|
|
524
|
-
const content = message.message?.content;
|
|
525
|
-
if (Array.isArray(content)) {
|
|
526
|
-
for (const block of content) {
|
|
527
|
-
if (block && block.type === "tool_result") {
|
|
528
|
-
logEvent("tool_result", stringifyResult(block.content));
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
} else if (message.type === "result") {
|
|
533
|
-
finalText = message.result ?? "";
|
|
534
|
-
logEvent("result", finalText);
|
|
535
|
-
} else if (message.type === "system") {
|
|
536
|
-
logEvent("system", JSON.stringify(message));
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
process.stdout.write("\n");
|
|
540
|
-
if (opts.abortController?.signal.aborted) {
|
|
541
|
-
throw new Error("Run canceled by user");
|
|
409
|
+
async function commitChanges(ctx, dir) {
|
|
410
|
+
if (!await hasChanges(dir)) return false;
|
|
411
|
+
try {
|
|
412
|
+
await git(["-C", dir, "commit", "--quiet", "-m", `FlumeCode: ${jobTitle(ctx)}`]);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
throw new PreCommitError(commitFailureLog(err));
|
|
542
415
|
}
|
|
543
|
-
return
|
|
416
|
+
return true;
|
|
544
417
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
var
|
|
549
|
-
|
|
418
|
+
async function pushBranch(ctx, dir) {
|
|
419
|
+
await git(["-C", dir, "push", "--quiet", "-u", "origin", ctx.repo.checkoutBranch]);
|
|
420
|
+
}
|
|
421
|
+
var RebaseConflictError = class extends Error {
|
|
422
|
+
constructor(mergeBranch, files) {
|
|
423
|
+
const list = files.length ? `: ${files.join(", ")}` : "";
|
|
424
|
+
super(`Rebase onto ${mergeBranch} hit conflicts in ${files.length} file(s)${list}`);
|
|
425
|
+
this.mergeBranch = mergeBranch;
|
|
426
|
+
this.files = files;
|
|
427
|
+
this.name = "RebaseConflictError";
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
async function rebaseOntoMergeBranch(ctx, dir) {
|
|
431
|
+
const { mergeBranch } = ctx.repo;
|
|
432
|
+
if (!mergeBranch) return;
|
|
433
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
|
|
550
434
|
try {
|
|
551
|
-
|
|
552
|
-
const run = (async () => {
|
|
553
|
-
for await (const message of query2({
|
|
554
|
-
prompt: "Reply with the single word: ok",
|
|
555
|
-
options: { permissionMode: "bypassPermissions", maxTurns: 1 }
|
|
556
|
-
})) {
|
|
557
|
-
if (message.type === "result") {
|
|
558
|
-
sawResult = true;
|
|
559
|
-
const isError = message.is_error === true;
|
|
560
|
-
const subtype = message.subtype;
|
|
561
|
-
if (isError)
|
|
562
|
-
throw new Error(
|
|
563
|
-
subtype ? `Claude Code error: ${subtype}` : "Claude Code returned an error"
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
})();
|
|
568
|
-
await withTimeout(run, PROBE_TIMEOUT_MS, "Claude Code did not respond in time");
|
|
569
|
-
if (!sawResult) return { ready: false, error: "Claude Code produced no result" };
|
|
570
|
-
return { ready: true };
|
|
435
|
+
await git(["-C", dir, "rebase", "--empty=drop", "FETCH_HEAD"]);
|
|
571
436
|
} catch (err) {
|
|
572
|
-
|
|
437
|
+
const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
|
|
438
|
+
() => ({ stdout: "" })
|
|
439
|
+
);
|
|
440
|
+
const files = conflicted.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
441
|
+
await git(["-C", dir, "rebase", "--abort"]).catch(() => {
|
|
442
|
+
});
|
|
443
|
+
if (files.length === 0) throw err;
|
|
444
|
+
throw new RebaseConflictError(mergeBranch, files);
|
|
573
445
|
}
|
|
574
446
|
}
|
|
575
|
-
function
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
reject(e);
|
|
586
|
-
}
|
|
587
|
-
);
|
|
588
|
-
});
|
|
447
|
+
async function mergeInMergeBranch(ctx, dir) {
|
|
448
|
+
const { mergeBranch } = ctx.repo;
|
|
449
|
+
if (!mergeBranch) return { conflicted: false };
|
|
450
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
|
|
451
|
+
try {
|
|
452
|
+
await git(["-C", dir, "merge", "--no-edit", "FETCH_HEAD"]);
|
|
453
|
+
return { conflicted: false };
|
|
454
|
+
} catch {
|
|
455
|
+
return { conflicted: true };
|
|
456
|
+
}
|
|
589
457
|
}
|
|
590
|
-
function
|
|
591
|
-
|
|
458
|
+
async function listUnmergedPaths(dir) {
|
|
459
|
+
const { stdout: stdout2 } = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(() => ({
|
|
460
|
+
stdout: ""
|
|
461
|
+
}));
|
|
462
|
+
return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
592
463
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
464
|
+
async function listConflictMarkerPaths(dir, paths) {
|
|
465
|
+
if (paths.length === 0) return [];
|
|
466
|
+
const { stdout: stdout2 } = await git([
|
|
467
|
+
"-C",
|
|
468
|
+
dir,
|
|
469
|
+
"grep",
|
|
470
|
+
"--no-color",
|
|
471
|
+
"-lE",
|
|
472
|
+
"^(<<<<<<<|>>>>>>>|\\|\\|\\|\\|\\|\\|\\|)",
|
|
473
|
+
"--",
|
|
474
|
+
...paths
|
|
475
|
+
]).catch(() => ({ stdout: "" }));
|
|
476
|
+
return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
602
477
|
}
|
|
603
|
-
function
|
|
604
|
-
const
|
|
605
|
-
|
|
478
|
+
async function openPullRequest(ctx) {
|
|
479
|
+
const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
|
|
480
|
+
if (!mergeBranch) return null;
|
|
481
|
+
const apiBase = `https://api.github.com/repos/${owner}/${name}`;
|
|
482
|
+
const headers = {
|
|
483
|
+
authorization: `Bearer ${cloneToken}`,
|
|
484
|
+
accept: "application/vnd.github+json",
|
|
485
|
+
"x-github-api-version": "2022-11-28",
|
|
486
|
+
"content-type": "application/json"
|
|
487
|
+
};
|
|
488
|
+
const title = jobTitle(ctx);
|
|
489
|
+
const body = ctx.kind === "init" ? "Bootstraps the `.flumecode/` wiki for this repository. Opened by the FlumeCode runner." : `Opened by the FlumeCode runner for request "${title}".`;
|
|
490
|
+
const res = await fetch(`${apiBase}/pulls`, {
|
|
491
|
+
method: "POST",
|
|
492
|
+
headers,
|
|
493
|
+
body: JSON.stringify({
|
|
494
|
+
title: `FlumeCode: ${title}`,
|
|
495
|
+
head: checkoutBranch,
|
|
496
|
+
base: mergeBranch,
|
|
497
|
+
body
|
|
498
|
+
})
|
|
499
|
+
});
|
|
500
|
+
if (res.status === 201) {
|
|
501
|
+
const data = await res.json();
|
|
502
|
+
return { number: data.number, url: data.html_url };
|
|
503
|
+
}
|
|
504
|
+
if (res.status === 422) {
|
|
505
|
+
const list = await fetch(
|
|
506
|
+
`${apiBase}/pulls?state=open&head=${owner}:${checkoutBranch}&base=${mergeBranch}`,
|
|
507
|
+
{ headers }
|
|
508
|
+
);
|
|
509
|
+
if (list.ok) {
|
|
510
|
+
const open = await list.json();
|
|
511
|
+
if (open[0]) return { number: open[0].number, url: open[0].html_url };
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
throw new Error(`PR creation failed: ${res.status} ${await res.text()}`);
|
|
606
516
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
function turnHeading(turn, agentName) {
|
|
610
|
-
if (turn.role === "user") return "User";
|
|
611
|
-
if (turn.failed) return `${agentName} (this run ended in an error)`;
|
|
612
|
-
if (turn.kind === "plan") return `${agentName} (proposed a plan)`;
|
|
613
|
-
if (turn.kind === "report") return `${agentName} (implementation report)`;
|
|
614
|
-
return agentName;
|
|
517
|
+
async function cleanup(dir) {
|
|
518
|
+
await rm(dir, { recursive: true, force: true });
|
|
615
519
|
}
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
520
|
+
function parsePrFromSubject(subject) {
|
|
521
|
+
const m = subject.match(/\(#(\d+)\)\s*$/);
|
|
522
|
+
return m ? Number(m[1]) : null;
|
|
523
|
+
}
|
|
524
|
+
async function incomingPrNumbers(ctx, dir, paths) {
|
|
525
|
+
if (!paths.length) return [];
|
|
526
|
+
try {
|
|
527
|
+
const mergeHeadResult = await git(["-C", dir, "rev-parse", "MERGE_HEAD"]);
|
|
528
|
+
const mergeHead = mergeHeadResult.stdout.trim();
|
|
529
|
+
const baseResult = await git(["-C", dir, "merge-base", "HEAD", mergeHead]);
|
|
530
|
+
const base = baseResult.stdout.trim();
|
|
531
|
+
const logResult = await git([
|
|
532
|
+
"-C",
|
|
533
|
+
dir,
|
|
534
|
+
"log",
|
|
535
|
+
"--no-merges",
|
|
536
|
+
`--format=%H%x1f%s`,
|
|
537
|
+
`${base}..${mergeHead}`,
|
|
538
|
+
"--",
|
|
539
|
+
...paths
|
|
540
|
+
]);
|
|
541
|
+
const nums = /* @__PURE__ */ new Set();
|
|
542
|
+
const needLookup = [];
|
|
543
|
+
for (const line of logResult.stdout.split("\n").filter(Boolean)) {
|
|
544
|
+
const idx = line.indexOf("");
|
|
545
|
+
const sha = line.slice(0, idx);
|
|
546
|
+
const subject = line.slice(idx + 1);
|
|
547
|
+
const n = parsePrFromSubject(subject);
|
|
548
|
+
if (n !== null) nums.add(n);
|
|
549
|
+
else needLookup.push(sha);
|
|
550
|
+
}
|
|
551
|
+
for (const sha of needLookup) {
|
|
552
|
+
for (const n of await prNumbersForCommit(ctx, sha)) nums.add(n);
|
|
553
|
+
}
|
|
554
|
+
return [...nums];
|
|
555
|
+
} catch {
|
|
556
|
+
return [];
|
|
621
557
|
}
|
|
622
558
|
}
|
|
623
|
-
function
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
"These coding guidelines apply to all code produced in this run:",
|
|
638
|
-
"",
|
|
639
|
-
loadRule("coding-guideline")
|
|
640
|
-
);
|
|
559
|
+
async function prNumbersForCommit(ctx, sha) {
|
|
560
|
+
const { owner, name, cloneToken } = ctx.repo;
|
|
561
|
+
try {
|
|
562
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${name}/commits/${sha}/pulls`, {
|
|
563
|
+
headers: {
|
|
564
|
+
authorization: `Bearer ${cloneToken}`,
|
|
565
|
+
accept: "application/vnd.github+json",
|
|
566
|
+
"x-github-api-version": "2022-11-28"
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
if (!res.ok) return [];
|
|
570
|
+
return (await res.json()).map((p) => p.number);
|
|
571
|
+
} catch {
|
|
572
|
+
return [];
|
|
641
573
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/plugins/manifest.ts
|
|
577
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
578
|
+
import { readdir as readdir2, readFile } from "node:fs/promises";
|
|
579
|
+
import { join as join3 } from "node:path";
|
|
580
|
+
async function loadPlugins(dir) {
|
|
581
|
+
const pluginsDir = join3(dir, ".flumecode", "plugins");
|
|
582
|
+
if (!existsSync3(pluginsDir)) return [];
|
|
583
|
+
let entries;
|
|
584
|
+
try {
|
|
585
|
+
entries = await readdir2(pluginsDir);
|
|
586
|
+
} catch {
|
|
587
|
+
return [];
|
|
645
588
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
""
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
589
|
+
const manifests = [];
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
const manifestPath = join3(pluginsDir, entry, "plugin.json");
|
|
592
|
+
try {
|
|
593
|
+
const raw = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
594
|
+
const manifest = parseManifest(raw);
|
|
595
|
+
if (manifest) manifests.push(manifest);
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return manifests;
|
|
600
|
+
}
|
|
601
|
+
function parseManifest(raw) {
|
|
602
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
603
|
+
const r = raw;
|
|
604
|
+
if (typeof r.key !== "string" || !r.key) return null;
|
|
605
|
+
if (r.socket !== "pre-commit") return null;
|
|
606
|
+
if (typeof r.run !== "string" || !r.run) return null;
|
|
607
|
+
const manifest = { key: r.key, socket: r.socket, run: r.run };
|
|
608
|
+
if (typeof r.heartbeat === "object" && r.heartbeat !== null) {
|
|
609
|
+
const hb = r.heartbeat;
|
|
610
|
+
if (typeof hb.url === "string" && typeof hb.token === "string") {
|
|
611
|
+
manifest.heartbeat = { url: hb.url, token: hb.token };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return manifest;
|
|
652
615
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
"",
|
|
664
|
-
"These coding guidelines apply to all code produced in this run:",
|
|
665
|
-
"",
|
|
666
|
-
loadRule("coding-guideline"),
|
|
667
|
-
"",
|
|
668
|
-
`# Plan: ${ctx.request?.title ?? ""}`
|
|
669
|
-
];
|
|
670
|
-
if (ctx.request?.body) {
|
|
671
|
-
lines2.push("", ctx.request.body);
|
|
672
|
-
}
|
|
673
|
-
appendThread(lines2, ctx);
|
|
674
|
-
lines2.push(
|
|
675
|
-
"",
|
|
676
|
-
"The last message above is the user's request for this turn. Your final reply is posted verbatim as your comment in the plan thread: if you implemented a change, make it a short report of what you changed (the runner appends the pull-request link); if you asked a question, called `submit_plan`, or pushed back, your reply text is posted as-is."
|
|
677
|
-
);
|
|
678
|
-
return lines2.join("\n");
|
|
679
|
-
}
|
|
680
|
-
function buildResolvePrompt(ctx) {
|
|
681
|
-
const mergeBranch = ctx.repo.mergeBranch ?? "the merge branch";
|
|
682
|
-
const task = `Use the \`flumecode:resolve-merge-conflict\` skill to handle this turn. A merge of \`${mergeBranch}\` into this branch is IN PROGRESS and has left conflict markers in your working tree. Resolve every conflicted file by correctly integrating BOTH sides \u2014 the change this session implemented (described below) and the incoming changes from \`${mergeBranch}\` \u2014 never blindly discard either side. Remove all conflict markers and verify the result builds and tests pass. Do NOT \`git add\`, commit, push, or open a pull request \u2014 the runner finalizes the merge commit and updates the existing pull request.`;
|
|
683
|
-
const orient = `Before investigating raw source, check for a FlumeCode wiki at \`.flumecode/wiki/\`. If it exists, read \`.flumecode/wiki/README.md\` first \u2014 it is the index \u2014 and follow its links to the pages and source paths relevant to the conflicting code. If there is no wiki, work from the code directly.`;
|
|
684
|
-
const lines2 = [
|
|
685
|
-
`You are "${ctx.agentName}", an autonomous coding agent resolving merge conflicts on an implemented FlumeCode plan.`,
|
|
686
|
-
`The repository ${ctx.repo.fullName} is checked out in your current working directory on the plan's implementation branch "${ctx.repo.checkoutBranch}" \u2014 the same branch its open pull request is built from \u2014 with an in-progress merge of "${mergeBranch}".`,
|
|
687
|
-
task,
|
|
688
|
-
orient,
|
|
689
|
-
"",
|
|
690
|
-
"These coding guidelines apply to all code produced in this run:",
|
|
691
|
-
"",
|
|
692
|
-
loadRule("coding-guideline"),
|
|
693
|
-
"",
|
|
694
|
-
`# Plan: ${ctx.request?.title ?? ""}`
|
|
695
|
-
];
|
|
696
|
-
if (ctx.request?.body) {
|
|
697
|
-
lines2.push("", ctx.request.body);
|
|
616
|
+
|
|
617
|
+
// src/plugins/socket.ts
|
|
618
|
+
var exec2 = promisify2(execCb);
|
|
619
|
+
async function runSocket(socketName, dir) {
|
|
620
|
+
const plugins = (await loadPlugins(dir)).filter((p) => p.socket === socketName);
|
|
621
|
+
for (const plugin of plugins) {
|
|
622
|
+
const result = await runPluginCommand(plugin.run, dir);
|
|
623
|
+
if (result.exitCode !== 0) {
|
|
624
|
+
throw new PreCommitError(`[plugin:${plugin.key}] ${result.output}`);
|
|
625
|
+
}
|
|
698
626
|
}
|
|
699
|
-
appendThread(lines2, ctx);
|
|
700
|
-
lines2.push(
|
|
701
|
-
"",
|
|
702
|
-
"Resolve the conflicts now. Your final reply is posted as a report in the plan thread: summarize which files conflicted and how you resolved each (the runner appends the pull-request link, so don't add one)."
|
|
703
|
-
);
|
|
704
|
-
return lines2.join("\n");
|
|
705
627
|
}
|
|
706
|
-
function
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
lines2.push("", ctx.request.body);
|
|
628
|
+
async function runPluginCommand(command2, cwd) {
|
|
629
|
+
try {
|
|
630
|
+
const result = await exec2(command2, { cwd, maxBuffer: 1 << 24 });
|
|
631
|
+
const output = [result.stdout, result.stderr].map((s) => s.trim()).filter(Boolean).join("\n");
|
|
632
|
+
return { exitCode: 0, output };
|
|
633
|
+
} catch (err) {
|
|
634
|
+
const e = err;
|
|
635
|
+
const output = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean).join("\n");
|
|
636
|
+
return { exitCode: typeof e.code === "number" ? e.code : 1, output };
|
|
716
637
|
}
|
|
717
|
-
appendThread(lines2, ctx);
|
|
718
|
-
lines2.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
|
|
719
|
-
return lines2.join("\n");
|
|
720
|
-
}
|
|
721
|
-
function buildRepairPrompt(ctx, hookLog) {
|
|
722
|
-
const lines2 = [
|
|
723
|
-
`You are "${ctx.agentName}", fixing a failed pre-commit check in the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
724
|
-
`The changes from the previous step are still uncommitted in the working tree. When the runner tried to commit them, the repository's pre-commit hook \u2014 which runs the project's own checks (lint / typecheck / unit tests) \u2014 failed. Make the working tree pass those checks: fix the failing code or tests at their root. Do NOT delete or skip tests, weaken assertions, or disable the checks to silence the failure. Preserve the intent of the original change; repair only what's broken. Do NOT commit or push \u2014 the runner re-commits once the checks pass.`,
|
|
725
|
-
"",
|
|
726
|
-
"These coding guidelines apply to all code produced in this run:",
|
|
727
|
-
"",
|
|
728
|
-
loadRule("coding-guideline"),
|
|
729
|
-
"",
|
|
730
|
-
"# Pre-commit hook output",
|
|
731
|
-
"",
|
|
732
|
-
"```",
|
|
733
|
-
hookLog,
|
|
734
|
-
"```",
|
|
735
|
-
"",
|
|
736
|
-
"When done, reply with a one-line summary of what you fixed."
|
|
737
|
-
];
|
|
738
|
-
return lines2.join("\n");
|
|
739
638
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
"",
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
lines2.push(
|
|
779
|
-
"",
|
|
780
|
-
"Your final reply is posted verbatim as your comment in the release thread \u2014 if you called widgets (Phase 1), your reply text accompanies the questions; if you applied the bumps (Phase 2), make it the report the skill produced. The runner appends the pull-request link."
|
|
639
|
+
|
|
640
|
+
// src/executor.ts
|
|
641
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
642
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
643
|
+
|
|
644
|
+
// src/widgets.ts
|
|
645
|
+
import { randomUUID } from "node:crypto";
|
|
646
|
+
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
647
|
+
import { z } from "zod";
|
|
648
|
+
var SERVER_NAME = "flume_widgets";
|
|
649
|
+
var SINGLE_SELECT = "single_select";
|
|
650
|
+
var MULTI_SELECT = "multi_select";
|
|
651
|
+
var WIDGET_TOOL_NAMES = [
|
|
652
|
+
`mcp__${SERVER_NAME}__${SINGLE_SELECT}`,
|
|
653
|
+
`mcp__${SERVER_NAME}__${MULTI_SELECT}`
|
|
654
|
+
];
|
|
655
|
+
var optionsSchema = z.array(z.string().min(1)).min(2).max(8).describe("2\u20138 short, distinct choices for the user to pick from.");
|
|
656
|
+
var TAIL = "Do NOT add an 'Other' or 'None of these' catch-all \u2014 the UI always offers an 'Other' free-text option automatically. After calling this, END YOUR TURN and wait: the user's answer arrives as their next message and starts a fresh run.";
|
|
657
|
+
function createWidgetTooling() {
|
|
658
|
+
const collected = [];
|
|
659
|
+
const singleSelect = tool(
|
|
660
|
+
SINGLE_SELECT,
|
|
661
|
+
"Ask the user a single-select (radio-button) question \u2014 exactly one answer. Use this for a genuine either/or choice (competing approaches, scope decisions, yes/no) instead of writing the options as prose. " + TAIL,
|
|
662
|
+
{
|
|
663
|
+
question: z.string().min(1).describe("The question to ask the user."),
|
|
664
|
+
options: optionsSchema
|
|
665
|
+
},
|
|
666
|
+
async (args) => {
|
|
667
|
+
collected.push({
|
|
668
|
+
id: randomUUID(),
|
|
669
|
+
type: "single_select",
|
|
670
|
+
question: args.question,
|
|
671
|
+
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
672
|
+
selectedOptionId: null,
|
|
673
|
+
customAnswer: null
|
|
674
|
+
});
|
|
675
|
+
return widgetPosted("single-select");
|
|
676
|
+
}
|
|
781
677
|
);
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
678
|
+
const multiSelect = tool(
|
|
679
|
+
MULTI_SELECT,
|
|
680
|
+
"Ask the user a multi-select (checkbox) question \u2014 they may pick any number of options, including none of the presets if they use 'Other'. Use this for 'select all that apply' questions (which features to include, which files to touch). " + TAIL,
|
|
681
|
+
{
|
|
682
|
+
question: z.string().min(1).describe("The question to ask the user."),
|
|
683
|
+
options: optionsSchema
|
|
684
|
+
},
|
|
685
|
+
async (args) => {
|
|
686
|
+
collected.push({
|
|
687
|
+
id: randomUUID(),
|
|
688
|
+
type: "multi_select",
|
|
689
|
+
question: args.question,
|
|
690
|
+
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
691
|
+
selectedOptionIds: null,
|
|
692
|
+
customAnswer: null
|
|
693
|
+
});
|
|
694
|
+
return widgetPosted("multi-select");
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
const mcpServer = createSdkMcpServer({
|
|
698
|
+
name: SERVER_NAME,
|
|
699
|
+
tools: [singleSelect, multiSelect]
|
|
700
|
+
});
|
|
701
|
+
return { mcpServer, collected };
|
|
791
702
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
703
|
+
function widgetPosted(kind) {
|
|
704
|
+
return {
|
|
705
|
+
content: [
|
|
706
|
+
{
|
|
707
|
+
type: "text",
|
|
708
|
+
text: `Question posted to the user as a ${kind} widget. End your turn now and wait \u2014 their answer will arrive as their next message.`
|
|
709
|
+
}
|
|
710
|
+
]
|
|
711
|
+
};
|
|
796
712
|
}
|
|
797
713
|
|
|
798
|
-
// src/
|
|
799
|
-
import {
|
|
800
|
-
import {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
var
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
714
|
+
// src/plan.ts
|
|
715
|
+
import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
716
|
+
import { z as z2 } from "zod";
|
|
717
|
+
var SERVER_NAME2 = "flume_plan";
|
|
718
|
+
var SUBMIT_PLAN = "submit_plan";
|
|
719
|
+
var PLAN_TOOL_NAME = `mcp__${SERVER_NAME2}__${SUBMIT_PLAN}`;
|
|
720
|
+
var PLAN_MARKER = "<!-- flumecode:end-of-plan -->";
|
|
721
|
+
var pseudoCodeEntrySchema = z2.object({
|
|
722
|
+
file: z2.string().min(1),
|
|
723
|
+
pseudoCode: z2.string().min(1)
|
|
724
|
+
});
|
|
725
|
+
var stepSchema = z2.object({
|
|
726
|
+
title: z2.string().min(1).describe("A concise imperative title for this step."),
|
|
727
|
+
description: z2.string().min(1).describe("What changes and why \u2014 the rationale for this step."),
|
|
728
|
+
pseudoCode: z2.array(pseudoCodeEntrySchema).optional().describe(
|
|
729
|
+
"Per-file pseudo code. Provide an entry for every non-documentation file this step touches. Each entry contains the file path and pseudo code describing the changes to that file."
|
|
730
|
+
)
|
|
731
|
+
});
|
|
732
|
+
var planInputSchema = {
|
|
733
|
+
title: z2.string().min(1).max(120).describe(
|
|
734
|
+
"A concise, descriptive name for THIS plan. Must be distinct from the request title and from any sibling plans on the same request. Keep it under 120 characters."
|
|
735
|
+
),
|
|
736
|
+
scope: z2.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
|
|
737
|
+
goal: z2.string().min(1).describe("One or two sentences stating the outcome."),
|
|
738
|
+
assumptions: z2.array(z2.string()).describe("Anything decided during planning, including unanswered defaults."),
|
|
739
|
+
steps: z2.array(stepSchema).min(1).describe("Ordered list of changes. Each step says what and why, with file references."),
|
|
740
|
+
acceptanceCriteria: z2.array(z2.string().min(1)).min(2).describe(
|
|
741
|
+
"Concrete, deterministically-checkable conditions that together define done. Each names a trigger/precondition and the exact observable result (run X -> output Y; file Z contains W; f(a) returns b) \u2014 no vague adjectives, not a restatement of a step. The set must collectively cover every step's change. At least 2 required."
|
|
742
|
+
),
|
|
743
|
+
risks: z2.array(z2.string()).describe("Anything that could change the approach."),
|
|
744
|
+
outOfScope: z2.array(z2.string()).describe("What is deliberately not being done.")
|
|
745
|
+
};
|
|
746
|
+
var planSchema = z2.object(planInputSchema);
|
|
747
|
+
function renderPlan(plan) {
|
|
748
|
+
const lines2 = [];
|
|
749
|
+
lines2.push(`# ${plan.title}`);
|
|
750
|
+
lines2.push("");
|
|
751
|
+
lines2.push(`**Scope** \u2014 \`${plan.scope}\``);
|
|
752
|
+
lines2.push("");
|
|
753
|
+
lines2.push(`**Goal** \u2014 ${plan.goal}`);
|
|
754
|
+
if (plan.assumptions.length > 0) {
|
|
755
|
+
lines2.push("");
|
|
756
|
+
lines2.push("**Assumptions**");
|
|
757
|
+
for (const assumption of plan.assumptions) {
|
|
758
|
+
lines2.push(`- ${assumption}`);
|
|
759
|
+
}
|
|
818
760
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
async function installDependencies(dir) {
|
|
837
|
-
const manager = detectPackageManager(dir);
|
|
838
|
-
if (manager === null) return { status: "skipped" };
|
|
839
|
-
const env = { ...process.env, CI: "1", ADBLOCK: "1", DISABLE_OPENCOLLECTIVE: "1" };
|
|
840
|
-
logEvent("install", `${manager} install`);
|
|
841
|
-
try {
|
|
842
|
-
const result = await exec(manager, ["install"], {
|
|
843
|
-
cwd: dir,
|
|
844
|
-
maxBuffer: MAX_BUFFER,
|
|
845
|
-
env,
|
|
846
|
-
timeout: 5 * 6e4
|
|
847
|
-
});
|
|
848
|
-
if (result.stdout.trim()) logEvent("install:out", result.stdout.trim());
|
|
849
|
-
if (result.stderr.trim()) logEvent("install:err", result.stderr.trim());
|
|
850
|
-
return { status: "installed", manager };
|
|
851
|
-
} catch (err) {
|
|
852
|
-
const e = err;
|
|
853
|
-
const detail = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean).join("\n");
|
|
854
|
-
logEvent("install:err", detail || (err instanceof Error ? err.message : String(err)));
|
|
855
|
-
return { status: "failed", manager, error: err instanceof Error ? err.message : String(err) };
|
|
761
|
+
lines2.push("");
|
|
762
|
+
lines2.push("## Steps");
|
|
763
|
+
for (const [i, step] of plan.steps.entries()) {
|
|
764
|
+
lines2.push("");
|
|
765
|
+
lines2.push(`### ${i + 1}. ${step.title}`);
|
|
766
|
+
lines2.push("");
|
|
767
|
+
lines2.push(step.description);
|
|
768
|
+
if (step.pseudoCode && step.pseudoCode.length > 0) {
|
|
769
|
+
for (const entry of step.pseudoCode) {
|
|
770
|
+
lines2.push("");
|
|
771
|
+
lines2.push(`\`${entry.file}\``);
|
|
772
|
+
lines2.push("");
|
|
773
|
+
lines2.push("```");
|
|
774
|
+
lines2.push(entry.pseudoCode);
|
|
775
|
+
lines2.push("```");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
856
778
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
var MAX_WORKSPACES = 8;
|
|
862
|
-
var workspaceRegistry = /* @__PURE__ */ new Map();
|
|
863
|
-
async function acquireWorkspace(key) {
|
|
864
|
-
const existing = workspaceRegistry.get(key);
|
|
865
|
-
if (existing !== void 0 && existsSync2(existing)) {
|
|
866
|
-
workspaceRegistry.delete(key);
|
|
867
|
-
workspaceRegistry.set(key, existing);
|
|
868
|
-
return { dir: existing, reused: true };
|
|
779
|
+
lines2.push("");
|
|
780
|
+
lines2.push("## Acceptance criteria");
|
|
781
|
+
for (const criterion of plan.acceptanceCriteria) {
|
|
782
|
+
lines2.push(`- [ ] ${criterion}`);
|
|
869
783
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
rm(oldDir, { recursive: true, force: true }).catch(() => {
|
|
877
|
-
});
|
|
784
|
+
if (plan.risks.length > 0) {
|
|
785
|
+
lines2.push("");
|
|
786
|
+
lines2.push("**Risks / open questions**");
|
|
787
|
+
for (const risk of plan.risks) {
|
|
788
|
+
lines2.push(`- ${risk}`);
|
|
789
|
+
}
|
|
878
790
|
}
|
|
879
|
-
|
|
791
|
+
if (plan.outOfScope.length > 0) {
|
|
792
|
+
lines2.push("");
|
|
793
|
+
lines2.push("**Out of scope**");
|
|
794
|
+
for (const item of plan.outOfScope) {
|
|
795
|
+
lines2.push(`- ${item}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
lines2.push("");
|
|
799
|
+
lines2.push(PLAN_MARKER);
|
|
800
|
+
return lines2.join("\n");
|
|
880
801
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
802
|
+
var submitPlanInputSchema = {
|
|
803
|
+
plans: z2.array(z2.object(planInputSchema)).min(1).refine(
|
|
804
|
+
(arr) => {
|
|
805
|
+
const titles = arr.map((p) => p.title.trim()).filter((t) => t.length > 0);
|
|
806
|
+
return new Set(titles).size === titles.length;
|
|
807
|
+
},
|
|
808
|
+
{ message: "Each plan must have a distinct non-empty title" }
|
|
809
|
+
)
|
|
810
|
+
};
|
|
811
|
+
var submitPlanSchema = z2.object(submitPlanInputSchema);
|
|
812
|
+
function createPlanTooling() {
|
|
813
|
+
let renderedPlans = null;
|
|
814
|
+
const submitPlan = tool2(
|
|
815
|
+
SUBMIT_PLAN,
|
|
816
|
+
"Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable Accept-as-plan draft. Do NOT call submit_plan more than once. acceptanceCriteria is required in each plan and must contain at least 2 observable, verifiable conditions. The 'title' field names each specific plan \u2014 make it concise and distinct from the request title and from sibling plan titles.",
|
|
817
|
+
submitPlanInputSchema,
|
|
818
|
+
async (args) => {
|
|
819
|
+
const parsed = submitPlanSchema.parse(args);
|
|
820
|
+
renderedPlans = parsed.plans.map(renderPlan);
|
|
821
|
+
return {
|
|
822
|
+
content: [
|
|
823
|
+
{
|
|
824
|
+
type: "text",
|
|
825
|
+
text: "Plan(s) submitted. The runner will render and post them as your comment(s). End your turn now."
|
|
826
|
+
}
|
|
827
|
+
]
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
const mcpServer = createSdkMcpServer2({
|
|
832
|
+
name: SERVER_NAME2,
|
|
833
|
+
tools: [submitPlan]
|
|
834
|
+
});
|
|
835
|
+
return { mcpServer, getPlans: () => renderedPlans };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/report.ts
|
|
839
|
+
import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
840
|
+
import { z as z3 } from "zod";
|
|
841
|
+
var SERVER_NAME3 = "flume_report";
|
|
842
|
+
var SUBMIT_REPORT = "submit_report";
|
|
843
|
+
var REPORT_TOOL_NAME = `mcp__${SERVER_NAME3}__${SUBMIT_REPORT}`;
|
|
844
|
+
var STATUS_ICON = {
|
|
845
|
+
met: "\u2705",
|
|
846
|
+
not_met: "\u274C",
|
|
847
|
+
unclear: "\u26A0\uFE0F"
|
|
848
|
+
};
|
|
849
|
+
var evidenceSchema = z3.object({
|
|
850
|
+
file: z3.string().min(1).describe("Repo-relative path the hunk comes from."),
|
|
851
|
+
hunk: z3.string().min(1).describe(
|
|
852
|
+
"A unified-diff hunk body proving the criterion \u2014 the lines that matter, not the whole file. Rendered verbatim as a ```diff block."
|
|
853
|
+
),
|
|
854
|
+
note: z3.string().optional().describe("Optional one-line explanation of why this hunk satisfies the criterion.")
|
|
855
|
+
});
|
|
856
|
+
var acVerdictSchema = z3.object({
|
|
857
|
+
criterion: z3.string().min(1).describe("The acceptance-criterion text, verbatim from the plan."),
|
|
858
|
+
status: z3.enum(["met", "not_met", "unclear"]).describe("Verdict for this criterion, verified against the actual diff."),
|
|
859
|
+
rationale: z3.string().min(1).describe("One or two sentences on why the verdict holds."),
|
|
860
|
+
evidence: z3.array(evidenceSchema).describe(
|
|
861
|
+
"Diff hunks proving the verdict, copied verbatim from git --no-pager diff. Across ALL criteria the evidence must collectively cover every hunk in the diff \u2014 each changed hunk appears under at least one criterion. Cite the relevant hunk(s) for a met criterion; may be empty for not_met / unclear."
|
|
862
|
+
)
|
|
863
|
+
});
|
|
864
|
+
var reportInputSchema = {
|
|
865
|
+
summary: z3.string().min(1).describe("One or two sentences on what was implemented."),
|
|
866
|
+
prose: z3.string().min(1).describe(
|
|
867
|
+
"Markdown for the remaining report sections \u2014 Files changed, Build / tests, and Caveats / follow-ups. Use ## headings. Do NOT include the acceptance-criteria section here (that goes in acceptanceCriteria) and do NOT include the PR link (the runner appends it)."
|
|
868
|
+
),
|
|
869
|
+
acceptanceCriteria: z3.array(acVerdictSchema).min(1).describe(
|
|
870
|
+
"One entry per acceptance criterion from the plan, in plan order, each with a verdict and the diff evidence behind it."
|
|
871
|
+
)
|
|
872
|
+
};
|
|
873
|
+
var reportSchema = z3.object(reportInputSchema);
|
|
874
|
+
function renderReport(report) {
|
|
875
|
+
const lines2 = [];
|
|
876
|
+
lines2.push(report.summary.trim());
|
|
877
|
+
lines2.push("");
|
|
878
|
+
lines2.push(report.prose.trim());
|
|
879
|
+
lines2.push("");
|
|
880
|
+
lines2.push("## Acceptance criteria");
|
|
881
|
+
for (const ac of report.acceptanceCriteria) {
|
|
882
|
+
lines2.push("");
|
|
883
|
+
lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
|
|
884
|
+
lines2.push("");
|
|
885
|
+
lines2.push(ac.rationale.trim());
|
|
886
|
+
for (const ev of ac.evidence) {
|
|
887
|
+
lines2.push("");
|
|
888
|
+
lines2.push(ev.note ? `\`${ev.file}\` \u2014 ${ev.note}` : `\`${ev.file}\``);
|
|
889
|
+
lines2.push("");
|
|
890
|
+
lines2.push("```diff");
|
|
891
|
+
lines2.push(ev.hunk.replace(/\n+$/, ""));
|
|
892
|
+
lines2.push("```");
|
|
893
|
+
}
|
|
887
894
|
}
|
|
895
|
+
return lines2.join("\n");
|
|
888
896
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
897
|
+
function createReportTooling() {
|
|
898
|
+
let submittedReport = null;
|
|
899
|
+
const submitReport = tool3(
|
|
900
|
+
SUBMIT_REPORT,
|
|
901
|
+
"Submit the final implementation report as structured data. Call this exactly once, at the end of the run. `acceptanceCriteria` must contain one entry per plan criterion, each with a met / not_met / unclear verdict and the diff hunk(s) that prove it. `summary` + `prose` are markdown for the rest of the report. Do NOT include a PR link \u2014 the runner appends it.",
|
|
902
|
+
reportInputSchema,
|
|
903
|
+
async (args) => {
|
|
904
|
+
submittedReport = reportSchema.parse(args);
|
|
905
|
+
return {
|
|
906
|
+
content: [
|
|
907
|
+
{
|
|
908
|
+
type: "text",
|
|
909
|
+
text: "Report submitted. The runner will render and post it. End your turn now."
|
|
910
|
+
}
|
|
911
|
+
]
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
const mcpServer = createSdkMcpServer3({
|
|
916
|
+
name: SERVER_NAME3,
|
|
917
|
+
tools: [submitReport]
|
|
893
918
|
});
|
|
919
|
+
return { mcpServer, getReport: () => submittedReport };
|
|
894
920
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
921
|
+
|
|
922
|
+
// src/executor.ts
|
|
923
|
+
var FLUME_PLUGIN_DIR = fileURLToPath2(new URL("../skills-plugin", import.meta.url));
|
|
924
|
+
function emptyUsage() {
|
|
925
|
+
return {
|
|
926
|
+
inputTokens: 0,
|
|
927
|
+
outputTokens: 0,
|
|
928
|
+
cacheCreationTokens: 0,
|
|
929
|
+
cacheReadTokens: 0,
|
|
930
|
+
costUsd: 0
|
|
931
|
+
};
|
|
904
932
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const result = await cloneResumingBranch(ctx, dir);
|
|
909
|
-
await ensureGitIdentity(dir, identity);
|
|
910
|
-
return result;
|
|
911
|
-
}
|
|
912
|
-
await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
|
|
913
|
-
await ensureGitIdentity(dir, identity);
|
|
914
|
-
return { resumed: true };
|
|
933
|
+
var usageAcc = emptyUsage();
|
|
934
|
+
function resetUsage() {
|
|
935
|
+
usageAcc = emptyUsage();
|
|
915
936
|
}
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
937
|
+
function getUsage() {
|
|
938
|
+
const totalTokens = usageAcc.inputTokens + usageAcc.outputTokens + usageAcc.cacheCreationTokens + usageAcc.cacheReadTokens;
|
|
939
|
+
return { ...usageAcc, totalTokens };
|
|
940
|
+
}
|
|
941
|
+
function stringifyResult(content) {
|
|
942
|
+
if (typeof content === "string") return content;
|
|
943
|
+
if (Array.isArray(content)) {
|
|
944
|
+
return content.map(
|
|
945
|
+
(c) => typeof c === "object" && c !== null && "text" in c ? String(c.text) : JSON.stringify(c)
|
|
946
|
+
).join("\n");
|
|
923
947
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
948
|
+
return JSON.stringify(content);
|
|
949
|
+
}
|
|
950
|
+
async function runClaudeCode(opts) {
|
|
951
|
+
let finalText = "";
|
|
952
|
+
const { mcpServer, collected } = createWidgetTooling();
|
|
953
|
+
const { mcpServer: planServer, getPlans } = createPlanTooling();
|
|
954
|
+
const { mcpServer: reportServer, getReport } = createReportTooling();
|
|
955
|
+
for await (const message of query({
|
|
956
|
+
prompt: opts.prompt,
|
|
957
|
+
options: {
|
|
958
|
+
cwd: opts.cwd,
|
|
959
|
+
permissionMode: opts.permissionMode,
|
|
960
|
+
allowDangerouslySkipPermissions: opts.permissionMode === "bypassPermissions",
|
|
961
|
+
...opts.model ? { model: opts.model } : {},
|
|
962
|
+
...opts.abortController ? { abortController: opts.abortController } : {},
|
|
963
|
+
maxTurns: opts.maxTurns ?? 40,
|
|
964
|
+
// Hermetic: ignore ALL config from the checked-out repo (CLAUDE.md,
|
|
965
|
+
// .claude/settings.json, .claude/skills, …) and the runner owner's
|
|
966
|
+
// ~/.claude. Only our plugin's skills are exposed to the agent. Auto-memory
|
|
967
|
+
// is disabled separately via CLAUDE_CODE_DISABLE_AUTO_MEMORY (see cli.ts).
|
|
968
|
+
settingSources: [],
|
|
969
|
+
plugins: [{ type: "local", path: FLUME_PLUGIN_DIR }],
|
|
970
|
+
// Pre-approve the widget, plan, and Task tools so they run without a
|
|
971
|
+
// permission prompt in headless mode (allow-listing pre-approves them; it
|
|
972
|
+
// does NOT restrict anything else). Task lets the implement-plan
|
|
973
|
+
// orchestrator spawn its subagents; without pre-approval the spawn could
|
|
974
|
+
// stall waiting for an approval no one can give.
|
|
975
|
+
mcpServers: { flume_widgets: mcpServer, flume_plan: planServer, flume_report: reportServer },
|
|
976
|
+
allowedTools: [...WIDGET_TOOL_NAMES, PLAN_TOOL_NAME, REPORT_TOOL_NAME, "Task"]
|
|
977
|
+
}
|
|
978
|
+
})) {
|
|
979
|
+
if (message.type === "assistant") {
|
|
980
|
+
const content = message.message?.content;
|
|
981
|
+
if (Array.isArray(content)) {
|
|
982
|
+
for (const block of content) {
|
|
983
|
+
if (block && block.type === "text" && typeof block.text === "string") {
|
|
984
|
+
process.stdout.write(block.text);
|
|
985
|
+
logEvent("agent", block.text);
|
|
986
|
+
} else if (block && block.type === "tool_use") {
|
|
987
|
+
logEvent("tool_use", `${block.name} ${JSON.stringify(block.input)}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} else if (message.type === "user") {
|
|
992
|
+
const content = message.message?.content;
|
|
993
|
+
if (Array.isArray(content)) {
|
|
994
|
+
for (const block of content) {
|
|
995
|
+
if (block && block.type === "tool_result") {
|
|
996
|
+
logEvent("tool_result", stringifyResult(block.content));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
} else if (message.type === "result") {
|
|
1001
|
+
finalText = message.result ?? "";
|
|
1002
|
+
logEvent("result", finalText);
|
|
1003
|
+
const resultMsg = message;
|
|
1004
|
+
if (resultMsg.usage) {
|
|
1005
|
+
usageAcc.inputTokens += resultMsg.usage.input_tokens ?? 0;
|
|
1006
|
+
usageAcc.outputTokens += resultMsg.usage.output_tokens ?? 0;
|
|
1007
|
+
usageAcc.cacheCreationTokens += resultMsg.usage.cache_creation_input_tokens ?? 0;
|
|
1008
|
+
usageAcc.cacheReadTokens += resultMsg.usage.cache_read_input_tokens ?? 0;
|
|
1009
|
+
}
|
|
1010
|
+
usageAcc.costUsd += resultMsg.total_cost_usd ?? 0;
|
|
1011
|
+
} else if (message.type === "system") {
|
|
1012
|
+
logEvent("system", JSON.stringify(message));
|
|
931
1013
|
}
|
|
932
1014
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1015
|
+
process.stdout.write("\n");
|
|
1016
|
+
if (opts.abortController?.signal.aborted) {
|
|
1017
|
+
throw new Error("Run canceled by user");
|
|
1018
|
+
}
|
|
1019
|
+
return { text: finalText, widgets: collected, plans: getPlans(), report: getReport() };
|
|
938
1020
|
}
|
|
939
|
-
|
|
940
|
-
|
|
1021
|
+
|
|
1022
|
+
// src/health.ts
|
|
1023
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1024
|
+
var PROBE_TIMEOUT_MS = 6e4;
|
|
1025
|
+
async function checkClaudeCode() {
|
|
941
1026
|
try {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1027
|
+
let sawResult = false;
|
|
1028
|
+
const run = (async () => {
|
|
1029
|
+
for await (const message of query2({
|
|
1030
|
+
prompt: "Reply with the single word: ok",
|
|
1031
|
+
options: { permissionMode: "bypassPermissions", maxTurns: 1 }
|
|
1032
|
+
})) {
|
|
1033
|
+
if (message.type === "result") {
|
|
1034
|
+
sawResult = true;
|
|
1035
|
+
const isError = message.is_error === true;
|
|
1036
|
+
const subtype = message.subtype;
|
|
1037
|
+
if (isError)
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
subtype ? `Claude Code error: ${subtype}` : "Claude Code returned an error"
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
})();
|
|
1044
|
+
await withTimeout(run, PROBE_TIMEOUT_MS, "Claude Code did not respond in time");
|
|
1045
|
+
if (!sawResult) return { ready: false, error: "Claude Code produced no result" };
|
|
1046
|
+
return { ready: true };
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
return { ready: false, error: errorMessage(err) };
|
|
948
1049
|
}
|
|
949
1050
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1051
|
+
function withTimeout(p, ms, message) {
|
|
1052
|
+
return new Promise((resolve, reject) => {
|
|
1053
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
1054
|
+
p.then(
|
|
1055
|
+
(v) => {
|
|
1056
|
+
clearTimeout(timer);
|
|
1057
|
+
resolve(v);
|
|
1058
|
+
},
|
|
1059
|
+
(e) => {
|
|
1060
|
+
clearTimeout(timer);
|
|
1061
|
+
reject(e);
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
});
|
|
954
1065
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
super("pre-commit checks failed");
|
|
958
|
-
this.log = log;
|
|
959
|
-
this.name = "PreCommitError";
|
|
960
|
-
}
|
|
961
|
-
};
|
|
962
|
-
function commitFailureLog(err) {
|
|
963
|
-
const e = err;
|
|
964
|
-
const parts = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter((s) => s.length > 0);
|
|
965
|
-
return parts.length > 0 ? parts.join("\n") : e.message ?? String(err);
|
|
1066
|
+
function errorMessage(err) {
|
|
1067
|
+
return err instanceof Error ? err.message : String(err);
|
|
966
1068
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1069
|
+
|
|
1070
|
+
// src/rules.ts
|
|
1071
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1072
|
+
import { join as join4 } from "node:path";
|
|
1073
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1074
|
+
var RULES_DIR = fileURLToPath3(new URL("../skills-plugin/rules", import.meta.url));
|
|
1075
|
+
function loadRule(name) {
|
|
1076
|
+
const raw = readFileSync3(join4(RULES_DIR, `${name}.md`), "utf8");
|
|
1077
|
+
return stripFrontMatter(raw).trim();
|
|
972
1078
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1079
|
+
function stripFrontMatter(raw) {
|
|
1080
|
+
const match = raw.match(/^---\n.*?\n---\n/s);
|
|
1081
|
+
return match ? raw.slice(match[0].length) : raw;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/prompt.ts
|
|
1085
|
+
function turnHeading(turn, agentName) {
|
|
1086
|
+
if (turn.role === "user") return "User";
|
|
1087
|
+
if (turn.failed) return `${agentName} (this run ended in an error)`;
|
|
1088
|
+
if (turn.kind === "plan") return `${agentName} (proposed a plan)`;
|
|
1089
|
+
if (turn.kind === "report") return `${agentName} (implementation report)`;
|
|
1090
|
+
return agentName;
|
|
1091
|
+
}
|
|
1092
|
+
function appendThread(lines2, ctx) {
|
|
1093
|
+
if (!ctx.thread || ctx.thread.length === 0) return;
|
|
1094
|
+
lines2.push("", "# Conversation so far");
|
|
1095
|
+
for (const turn of ctx.thread) {
|
|
1096
|
+
lines2.push("", `## ${turnHeading(turn, ctx.agentName)}`, turn.content);
|
|
986
1097
|
}
|
|
987
1098
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1099
|
+
function buildPrompt(ctx) {
|
|
1100
|
+
const task = ctx.permissionMode === "plan" ? `Use the \`flumecode:request-to-plan\` skill to handle this request. You are read-only and cannot modify files \u2014 clarify any ambiguity with the user first, then produce a concrete, actionable plan (the specific changes you would make and why). Cite the relevant files. Do NOT call ExitPlanMode or write the plan to a file. When the plan is ready, call the \`submit_plan\` tool with the structured plan fields; the runner renders it into the canonical plan markdown and posts it as your comment.` : `Use the \`flumecode:implement-plan\` skill to handle this request. You are the ORCHESTRATOR: do not implement, review, or write the report yourself \u2014 follow the skill to delegate each phase to subagents via the Task tool, picking the right model for each. Do not commit or push \u2014 the runner handles that.`;
|
|
1101
|
+
const orient = `Before investigating raw source, check for a FlumeCode wiki at \`.flumecode/wiki/\`. If it exists, read \`.flumecode/wiki/README.md\` first \u2014 it is the index \u2014 and follow its links to the pages and source paths relevant to this request. If there is no wiki, work from the code directly.`;
|
|
1102
|
+
const widgets = `When you need the user to choose, ask it as a widget rather than writing the options as prose: call \`single_select\` for a one-of-N choice (radio buttons) or \`multi_select\` for a "select all that apply" choice (checkboxes). Don't add your own "Other" option \u2014 the UI always provides one. After calling a widget tool, end your turn \u2014 the user's answer comes back as their next message and starts a fresh run.`;
|
|
1103
|
+
const lines2 = [
|
|
1104
|
+
`You are "${ctx.agentName}", an autonomous coding agent working inside a FlumeCode request.`,
|
|
1105
|
+
`The repository ${ctx.repo.fullName} is checked out in your current working directory on branch "${ctx.repo.checkoutBranch}" at commit ${ctx.repo.checkoutSha.slice(0, 7)}.`,
|
|
1106
|
+
task,
|
|
1107
|
+
orient,
|
|
1108
|
+
widgets
|
|
1109
|
+
];
|
|
1110
|
+
if (ctx.permissionMode !== "plan") {
|
|
1111
|
+
lines2.push(
|
|
1112
|
+
"",
|
|
1113
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
1114
|
+
"",
|
|
1115
|
+
loadRule("coding-guideline")
|
|
1116
|
+
);
|
|
994
1117
|
}
|
|
995
|
-
|
|
1118
|
+
lines2.push("", `# Request: ${ctx.request?.title ?? ""}`);
|
|
1119
|
+
if (ctx.request?.body) {
|
|
1120
|
+
lines2.push("", ctx.request.body);
|
|
1121
|
+
}
|
|
1122
|
+
appendThread(lines2, ctx);
|
|
1123
|
+
lines2.push(
|
|
1124
|
+
"",
|
|
1125
|
+
ctx.permissionMode === "plan" ? "Your final reply is posted verbatim as your comment in the thread \u2014 if you called `submit_plan`, the rendered plan is posted automatically; for clarifying questions, your reply text is posted as-is." : "Your final reply is posted verbatim as your comment in the thread \u2014 make it the implementation report your report subagent produced, with nothing added. The runner appends the pull-request link."
|
|
1126
|
+
);
|
|
1127
|
+
return lines2.join("\n");
|
|
996
1128
|
}
|
|
997
|
-
|
|
998
|
-
|
|
1129
|
+
function buildRevisePrompt(ctx) {
|
|
1130
|
+
const task = `Use the \`flumecode:revise-implementation\` skill to handle this turn. The plan below was already implemented (its implementation report appears in the conversation below, tagged as such); the user is now asking to fine-tune that implementation. Decide how to respond to their latest message: if it's unclear, ask a clarifying question (as a widget); if it's a bad idea or not feasible, push back with your reasoning; if it warrants rethinking the plan, call \`submit_plan\` with a revised plan; otherwise implement the requested change. When you implement, you are the ORCHESTRATOR: delegate the work to subagents via the Task tool as the skill directs, and do not commit or push \u2014 the runner handles that, updating the existing pull request.`;
|
|
1131
|
+
const orient = `Before investigating raw source, check for a FlumeCode wiki at \`.flumecode/wiki/\`. If it exists, read \`.flumecode/wiki/README.md\` first \u2014 it is the index \u2014 and follow its links to the pages and source paths relevant to this change. If there is no wiki, work from the code directly.`;
|
|
1132
|
+
const widgets = `When you need the user to choose, ask it as a widget rather than writing the options as prose: call \`single_select\` for a one-of-N choice (radio buttons) or \`multi_select\` for a "select all that apply" choice (checkboxes). Don't add your own "Other" option \u2014 the UI always provides one. After calling a widget tool, end your turn \u2014 the user's answer comes back as their next message and starts a fresh run.`;
|
|
1133
|
+
const lines2 = [
|
|
1134
|
+
`You are "${ctx.agentName}", an autonomous coding agent fine-tuning an implemented FlumeCode plan in an ongoing thread with the user.`,
|
|
1135
|
+
`The repository ${ctx.repo.fullName} is checked out in your current working directory on the plan's implementation branch "${ctx.repo.checkoutBranch}" \u2014 the same branch its open pull request is built from, so any change you push updates that PR.`,
|
|
1136
|
+
task,
|
|
1137
|
+
orient,
|
|
1138
|
+
widgets,
|
|
1139
|
+
"",
|
|
1140
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
1141
|
+
"",
|
|
1142
|
+
loadRule("coding-guideline"),
|
|
1143
|
+
"",
|
|
1144
|
+
`# Plan: ${ctx.request?.title ?? ""}`
|
|
1145
|
+
];
|
|
1146
|
+
if (ctx.request?.body) {
|
|
1147
|
+
lines2.push("", ctx.request.body);
|
|
1148
|
+
}
|
|
1149
|
+
appendThread(lines2, ctx);
|
|
1150
|
+
lines2.push(
|
|
1151
|
+
"",
|
|
1152
|
+
"The last message above is the user's request for this turn. Your final reply is posted verbatim as your comment in the plan thread: if you implemented a change, make it a short report of what you changed (the runner appends the pull-request link); if you asked a question, called `submit_plan`, or pushed back, your reply text is posted as-is."
|
|
1153
|
+
);
|
|
1154
|
+
return lines2.join("\n");
|
|
999
1155
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1156
|
+
function buildResolvePrompt(ctx, related = []) {
|
|
1157
|
+
const mergeBranch = ctx.repo.mergeBranch ?? "the merge branch";
|
|
1158
|
+
const task = `Use the \`flumecode:resolve-merge-conflict\` skill to handle this turn. A merge of \`${mergeBranch}\` into this branch is IN PROGRESS and has left conflict markers in your working tree. Resolve every conflicted file by correctly integrating BOTH sides \u2014 the change this session implemented (described below) and the incoming changes from \`${mergeBranch}\` \u2014 never blindly discard either side. Remove all conflict markers and verify the result builds and tests pass. Do NOT \`git add\`, commit, push, or open a pull request \u2014 the runner finalizes the merge commit and updates the existing pull request.`;
|
|
1159
|
+
const orient = `Before investigating raw source, check for a FlumeCode wiki at \`.flumecode/wiki/\`. If it exists, read \`.flumecode/wiki/README.md\` first \u2014 it is the index \u2014 and follow its links to the pages and source paths relevant to the conflicting code. If there is no wiki, work from the code directly.`;
|
|
1160
|
+
const lines2 = [
|
|
1161
|
+
`You are "${ctx.agentName}", an autonomous coding agent resolving merge conflicts on an implemented FlumeCode plan.`,
|
|
1162
|
+
`The repository ${ctx.repo.fullName} is checked out in your current working directory on the plan's implementation branch "${ctx.repo.checkoutBranch}" \u2014 the same branch its open pull request is built from \u2014 with an in-progress merge of "${mergeBranch}".`,
|
|
1163
|
+
task,
|
|
1164
|
+
orient,
|
|
1165
|
+
"",
|
|
1166
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
1167
|
+
"",
|
|
1168
|
+
loadRule("coding-guideline"),
|
|
1169
|
+
"",
|
|
1170
|
+
`# Plan: ${ctx.request?.title ?? ""}`
|
|
1171
|
+
];
|
|
1172
|
+
if (ctx.request?.body) {
|
|
1173
|
+
lines2.push("", ctx.request.body);
|
|
1007
1174
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
await git(["-C", dir, "rebase", "--empty=drop", "FETCH_HEAD"]);
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
|
|
1017
|
-
() => ({ stdout: "" })
|
|
1175
|
+
appendThread(lines2, ctx);
|
|
1176
|
+
if (related.length > 0) {
|
|
1177
|
+
lines2.push(
|
|
1178
|
+
"",
|
|
1179
|
+
"# Related sessions behind the incoming changes",
|
|
1180
|
+
"Each conflicting change on the merge branch came from another coding session whose plan and report follow. Preserve THEIR intent too while integrating them with this session's work \u2014 do not undo what they built."
|
|
1018
1181
|
);
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1182
|
+
for (const r of related) {
|
|
1183
|
+
lines2.push("", `## PR #${r.prNumber} \u2014 ${r.title}`);
|
|
1184
|
+
if (r.plan) lines2.push("", "### Accepted plan", r.plan);
|
|
1185
|
+
if (r.report) lines2.push("", "### Final report", r.report);
|
|
1186
|
+
}
|
|
1024
1187
|
}
|
|
1188
|
+
lines2.push(
|
|
1189
|
+
"",
|
|
1190
|
+
"Resolve the conflicts now. Your final reply is posted as a report in the plan thread: summarize which files conflicted and how you resolved each (the runner appends the pull-request link, so don't add one)."
|
|
1191
|
+
);
|
|
1192
|
+
return lines2.join("\n");
|
|
1025
1193
|
}
|
|
1026
|
-
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1194
|
+
function buildDocumentPrompt(ctx) {
|
|
1195
|
+
const lines2 = [
|
|
1196
|
+
`You are "${ctx.agentName}" maintaining the repository wiki for ${ctx.repo.fullName}.`,
|
|
1197
|
+
`An implementation just ran in this working directory to satisfy the request below; its changes are uncommitted in the working tree.`,
|
|
1198
|
+
`Use the \`flumecode:document\` skill to bring the wiki in sync with those changes. Only edit files under \`.flumecode/wiki/\` \u2014 do not touch application code. The runner commits the wiki alongside the implementation in the same pull request.`,
|
|
1199
|
+
"",
|
|
1200
|
+
`# Request: ${ctx.request?.title ?? ""}`
|
|
1201
|
+
];
|
|
1202
|
+
if (ctx.request?.body) {
|
|
1203
|
+
lines2.push("", ctx.request.body);
|
|
1035
1204
|
}
|
|
1205
|
+
appendThread(lines2, ctx);
|
|
1206
|
+
lines2.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
|
|
1207
|
+
return lines2.join("\n");
|
|
1036
1208
|
}
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1209
|
+
function buildRepairPrompt(ctx, hookLog) {
|
|
1210
|
+
const lines2 = [
|
|
1211
|
+
`You are "${ctx.agentName}", fixing a failed pre-commit check in the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
1212
|
+
`The changes from the previous step are still uncommitted in the working tree. When the runner tried to commit them, the repository's pre-commit hook \u2014 which runs the project's own checks (lint / typecheck / unit tests) \u2014 failed. Make the working tree pass those checks: fix the failing code or tests at their root. Do NOT delete or skip tests, weaken assertions, or disable the checks to silence the failure. Preserve the intent of the original change; repair only what's broken. Do NOT commit or push \u2014 the runner re-commits once the checks pass.`,
|
|
1213
|
+
"",
|
|
1214
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
1215
|
+
"",
|
|
1216
|
+
loadRule("coding-guideline"),
|
|
1217
|
+
"",
|
|
1218
|
+
"# Pre-commit hook output",
|
|
1219
|
+
"",
|
|
1220
|
+
"```",
|
|
1221
|
+
hookLog,
|
|
1222
|
+
"```",
|
|
1223
|
+
"",
|
|
1224
|
+
"When done, reply with a one-line summary of what you fixed."
|
|
1225
|
+
];
|
|
1226
|
+
return lines2.join("\n");
|
|
1042
1227
|
}
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
const
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
body
|
|
1063
|
-
})
|
|
1064
|
-
});
|
|
1065
|
-
if (res.status === 201) {
|
|
1066
|
-
const data = await res.json();
|
|
1067
|
-
return { number: data.number, url: data.html_url };
|
|
1228
|
+
function buildReleasePrompt(ctx, baseChecks) {
|
|
1229
|
+
const task = `Use the \`flumecode:create-release\` skill to handle this turn. You are driving a release: first analyse commits since the last tag, propose version bumps, and ask the user to confirm via widgets (Phase 1); once the user's widget answers appear in the thread, apply the bumps to package.json files and update CHANGELOG.md (Phase 2). Do NOT commit or push \u2014 the runner handles that and opens the bump PR.`;
|
|
1230
|
+
const orient = `Before investigating raw source, check for a FlumeCode wiki at \`.flumecode/wiki/\`. If it exists, read \`.flumecode/wiki/README.md\` first \u2014 it is the index \u2014 and follow its links to the pages and source paths relevant to this release. If there is no wiki, work from the code directly.`;
|
|
1231
|
+
const widgets = `When you need the user to choose, ask it as a widget rather than writing the options as prose: call \`single_select\` for a one-of-N choice (radio buttons) or \`multi_select\` for a "select all that apply" choice (checkboxes). Don't add your own "Other" option \u2014 the UI always provides one. After calling a widget tool, end your turn \u2014 the user's answer comes back as their next message and starts a fresh run.`;
|
|
1232
|
+
const lines2 = [
|
|
1233
|
+
`You are "${ctx.agentName}", an autonomous coding agent driving a FlumeCode release.`,
|
|
1234
|
+
`The repository ${ctx.repo.fullName} is checked out in your current working directory on the release bump branch "${ctx.repo.checkoutBranch}".`,
|
|
1235
|
+
task,
|
|
1236
|
+
orient,
|
|
1237
|
+
widgets,
|
|
1238
|
+
"",
|
|
1239
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
1240
|
+
"",
|
|
1241
|
+
loadRule("coding-guideline"),
|
|
1242
|
+
"",
|
|
1243
|
+
`# Release: ${ctx.request?.title ?? ""}`
|
|
1244
|
+
];
|
|
1245
|
+
if (ctx.request?.body) {
|
|
1246
|
+
lines2.push("", ctx.request.body);
|
|
1068
1247
|
}
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1248
|
+
if (baseChecks && !baseChecks.ok) {
|
|
1249
|
+
lines2.push(
|
|
1250
|
+
"",
|
|
1251
|
+
"# Pre-release check status",
|
|
1252
|
+
"",
|
|
1253
|
+
"\u26A0\uFE0F The repository's pre-commit checks (lint / typecheck / tests) are currently FAILING on the base branch, independently of any version bump. A release must not ship a broken base:",
|
|
1254
|
+
"",
|
|
1255
|
+
"- **Phase 1 (propose):** tell the user, in your reply, that the base currently fails these checks and that the release will fix them as part of the bump.",
|
|
1256
|
+
"- **Phase 2 (apply):** fix the failing code at its root so the checks pass, THEN apply the version bumps and CHANGELOG. Do NOT delete/skip tests or weaken assertions. The fixes ship in the same bump PR. Still do NOT commit or push \u2014 the runner does.",
|
|
1257
|
+
"",
|
|
1258
|
+
"Failing check output:",
|
|
1259
|
+
"",
|
|
1260
|
+
"```",
|
|
1261
|
+
baseChecks.log,
|
|
1262
|
+
"```"
|
|
1073
1263
|
);
|
|
1074
|
-
if (list.ok) {
|
|
1075
|
-
const open = await list.json();
|
|
1076
|
-
if (open[0]) return { number: open[0].number, url: open[0].html_url };
|
|
1077
|
-
}
|
|
1078
|
-
return null;
|
|
1079
1264
|
}
|
|
1080
|
-
|
|
1265
|
+
appendThread(lines2, ctx);
|
|
1266
|
+
lines2.push(
|
|
1267
|
+
"",
|
|
1268
|
+
"Your final reply is posted verbatim as your comment in the release thread \u2014 if you called widgets (Phase 1), your reply text accompanies the questions; if you applied the bumps (Phase 2), make it the report the skill produced. The runner appends the pull-request link."
|
|
1269
|
+
);
|
|
1270
|
+
return lines2.join("\n");
|
|
1081
1271
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1272
|
+
function buildInitPrompt(ctx) {
|
|
1273
|
+
return [
|
|
1274
|
+
`You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
1275
|
+
`Use the \`flumecode:document\` skill to create the initial repository wiki under \`.flumecode/wiki/\`. The wiki does not exist yet, so the skill will bootstrap it: survey the codebase and produce a high-level overview plus per-component pages. Only create files under \`.flumecode/\` \u2014 do not modify application code. The runner commits the result and opens a pull request.`,
|
|
1276
|
+
"",
|
|
1277
|
+
"When done, reply with a one- or two-line summary of the wiki you created."
|
|
1278
|
+
].join("\n");
|
|
1084
1279
|
}
|
|
1085
1280
|
|
|
1086
1281
|
// src/run.ts
|
|
@@ -1093,7 +1288,7 @@ var MAX_IMPLEMENT_RETRIES = 1;
|
|
|
1093
1288
|
var INIT_MAX_TURNS = 200;
|
|
1094
1289
|
var DOCUMENT_MAX_TURNS = 120;
|
|
1095
1290
|
var HEARTBEAT_MS = 5 * 6e4;
|
|
1096
|
-
async function pushAndOpenPr(ctx, dir, abort, opts = { rebase: true }) {
|
|
1291
|
+
async function pushAndOpenPr(ctx, dir, config, abort, opts = { rebase: true }) {
|
|
1097
1292
|
if (abort.signal.aborted) throw new Error("Run canceled by user");
|
|
1098
1293
|
const committed = await commitWithRepair(ctx, dir, abort);
|
|
1099
1294
|
if (!committed) return { outcome: { kind: "none" }, autoMerged: false };
|
|
@@ -1107,8 +1302,8 @@ async function pushAndOpenPr(ctx, dir, abort, opts = { rebase: true }) {
|
|
|
1107
1302
|
console.warn(
|
|
1108
1303
|
` rebase onto ${ctx.repo.mergeBranch} conflicted \u2014 merging it in and resolving with the agent\u2026`
|
|
1109
1304
|
);
|
|
1110
|
-
await mergeAndResolveConflicts(ctx, dir, abort);
|
|
1111
|
-
await commitWithRepair(ctx, dir, abort);
|
|
1305
|
+
await mergeAndResolveConflicts(ctx, dir, config, abort);
|
|
1306
|
+
await commitWithRepair(ctx, dir, abort, { skipSocket: true });
|
|
1112
1307
|
autoMerged = true;
|
|
1113
1308
|
}
|
|
1114
1309
|
}
|
|
@@ -1116,28 +1311,36 @@ async function pushAndOpenPr(ctx, dir, abort, opts = { rebase: true }) {
|
|
|
1116
1311
|
const pr = await openPullRequest(ctx);
|
|
1117
1312
|
return { outcome: pr ? { kind: "pr", pr } : { kind: "pushed" }, autoMerged };
|
|
1118
1313
|
}
|
|
1119
|
-
async function mergeAndResolveConflicts(ctx, dir, abort) {
|
|
1314
|
+
async function mergeAndResolveConflicts(ctx, dir, config, abort) {
|
|
1120
1315
|
const { conflicted } = await mergeInMergeBranch(ctx, dir);
|
|
1121
1316
|
if (!conflicted) return { resolved: false, text: null };
|
|
1317
|
+
const conflictedPaths = await listUnmergedPaths(dir);
|
|
1318
|
+
const prNumbers = await incomingPrNumbers(ctx, dir, conflictedPaths);
|
|
1319
|
+
const related = await fetchRelatedSessions(config, {
|
|
1320
|
+
owner: ctx.repo.owner,
|
|
1321
|
+
name: ctx.repo.name,
|
|
1322
|
+
prNumbers
|
|
1323
|
+
});
|
|
1122
1324
|
const result = await runClaudeCode({
|
|
1123
1325
|
cwd: dir,
|
|
1124
|
-
prompt: buildResolvePrompt(ctx),
|
|
1326
|
+
prompt: buildResolvePrompt(ctx, related),
|
|
1125
1327
|
permissionMode: ctx.permissionMode,
|
|
1126
1328
|
model: ORCHESTRATOR_MODEL,
|
|
1127
1329
|
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1128
1330
|
abortController: abort
|
|
1129
1331
|
});
|
|
1130
|
-
const unresolved = await
|
|
1332
|
+
const unresolved = await listConflictMarkerPaths(dir, conflictedPaths);
|
|
1131
1333
|
if (unresolved.length > 0) {
|
|
1132
1334
|
throw new Error(
|
|
1133
|
-
`Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still conflict: ${unresolved.join(", ")}`
|
|
1335
|
+
`Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still contain conflict markers: ${unresolved.join(", ")}`
|
|
1134
1336
|
);
|
|
1135
1337
|
}
|
|
1136
1338
|
return { resolved: true, text: result.text.trim() || null };
|
|
1137
1339
|
}
|
|
1138
|
-
async function commitWithRepair(ctx, dir, abort) {
|
|
1340
|
+
async function commitWithRepair(ctx, dir, abort, opts = {}) {
|
|
1139
1341
|
for (let attempt = 1; ; attempt++) {
|
|
1140
1342
|
try {
|
|
1343
|
+
if (!opts.skipSocket) await runSocket("pre-commit", dir);
|
|
1141
1344
|
return await commitChanges(ctx, dir);
|
|
1142
1345
|
} catch (err) {
|
|
1143
1346
|
if (!(err instanceof PreCommitError) || attempt > MAX_COMMIT_REPAIRS) throw err;
|
|
@@ -1176,38 +1379,38 @@ function outcomeBanner(outcome, opts) {
|
|
|
1176
1379
|
\u2139\uFE0F **No code changes were made** \u2014 ${opts.noChange ?? "there was nothing to open a pull request for."}`;
|
|
1177
1380
|
}
|
|
1178
1381
|
}
|
|
1179
|
-
async function processJob(ctx, abort = new AbortController()) {
|
|
1382
|
+
async function processJob(ctx, config, abort = new AbortController()) {
|
|
1180
1383
|
const { dir, reused } = await acquireWorkspace(ctx.workspaceKey);
|
|
1181
1384
|
let prepared = false;
|
|
1182
1385
|
try {
|
|
1183
1386
|
if (ctx.kind === "init") {
|
|
1184
1387
|
await prepareAtSha(ctx, dir, reused);
|
|
1185
1388
|
prepared = true;
|
|
1186
|
-
return await processInitJob(ctx, dir, abort);
|
|
1389
|
+
return await processInitJob(ctx, dir, config, abort);
|
|
1187
1390
|
}
|
|
1188
1391
|
if (ctx.kind === "implement") {
|
|
1189
1392
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
1190
1393
|
prepared = true;
|
|
1191
|
-
return await processImplementJob(ctx, dir, resumed, abort);
|
|
1394
|
+
return await processImplementJob(ctx, dir, resumed, config, abort);
|
|
1192
1395
|
}
|
|
1193
1396
|
if (ctx.kind === "revise") {
|
|
1194
1397
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
1195
1398
|
prepared = true;
|
|
1196
|
-
return await processReviseJob(ctx, dir, resumed, abort);
|
|
1399
|
+
return await processReviseJob(ctx, dir, resumed, config, abort);
|
|
1197
1400
|
}
|
|
1198
1401
|
if (ctx.kind === "resolve") {
|
|
1199
1402
|
await prepareResumingBranch(ctx, dir, reused);
|
|
1200
1403
|
prepared = true;
|
|
1201
|
-
return await processResolveJob(ctx, dir, abort);
|
|
1404
|
+
return await processResolveJob(ctx, dir, config, abort);
|
|
1202
1405
|
}
|
|
1203
1406
|
if (ctx.kind === "release") {
|
|
1204
1407
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
1205
1408
|
prepared = true;
|
|
1206
|
-
return await processReleaseJob(ctx, dir, resumed, abort);
|
|
1409
|
+
return await processReleaseJob(ctx, dir, resumed, config, abort);
|
|
1207
1410
|
}
|
|
1208
1411
|
await prepareAtSha(ctx, dir, reused);
|
|
1209
1412
|
prepared = true;
|
|
1210
|
-
return await processChatJob(ctx, dir, abort);
|
|
1413
|
+
return await processChatJob(ctx, dir, config, abort);
|
|
1211
1414
|
} catch (err) {
|
|
1212
1415
|
if (abort.signal.aborted && prepared) {
|
|
1213
1416
|
await resetWorkspace(dir);
|
|
@@ -1217,7 +1420,7 @@ async function processJob(ctx, abort = new AbortController()) {
|
|
|
1217
1420
|
throw err;
|
|
1218
1421
|
}
|
|
1219
1422
|
}
|
|
1220
|
-
async function processInitJob(ctx, dir, abort) {
|
|
1423
|
+
async function processInitJob(ctx, dir, config, abort) {
|
|
1221
1424
|
console.log(`
|
|
1222
1425
|
\u25B6 Init ${ctx.jobId} \u2014 ${ctx.repo.fullName} on ${ctx.repo.checkoutBranch}`);
|
|
1223
1426
|
const summary = (await runClaudeCode({
|
|
@@ -1228,7 +1431,7 @@ async function processInitJob(ctx, dir, abort) {
|
|
|
1228
1431
|
abortController: abort
|
|
1229
1432
|
})).text.trim();
|
|
1230
1433
|
let reply = summary || "(the agent produced no summary)";
|
|
1231
|
-
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort);
|
|
1434
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort);
|
|
1232
1435
|
reply += outcomeBanner(outcome, {
|
|
1233
1436
|
branch: ctx.repo.checkoutBranch,
|
|
1234
1437
|
noChange: "no files were generated; the wiki may already exist.",
|
|
@@ -1236,7 +1439,7 @@ async function processInitJob(ctx, dir, abort) {
|
|
|
1236
1439
|
});
|
|
1237
1440
|
return { text: reply, widgets: [] };
|
|
1238
1441
|
}
|
|
1239
|
-
async function processChatJob(ctx, dir, abort) {
|
|
1442
|
+
async function processChatJob(ctx, dir, config, abort) {
|
|
1240
1443
|
console.log(`
|
|
1241
1444
|
\u25B6 Job ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1242
1445
|
const orchestrating = ctx.permissionMode !== "plan";
|
|
@@ -1278,14 +1481,14 @@ async function processChatJob(ctx, dir, abort) {
|
|
|
1278
1481
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
1279
1482
|
}
|
|
1280
1483
|
}
|
|
1281
|
-
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort);
|
|
1484
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort);
|
|
1282
1485
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
1283
1486
|
return { text: reply, widgets: [] };
|
|
1284
1487
|
}
|
|
1285
1488
|
function reportClaimsWork(report) {
|
|
1286
1489
|
return !!report && report.acceptanceCriteria.some((ac) => ac.status === "met" && ac.evidence.length > 0);
|
|
1287
1490
|
}
|
|
1288
|
-
async function processImplementJob(ctx, dir, resumed, abort) {
|
|
1491
|
+
async function processImplementJob(ctx, dir, resumed, config, abort) {
|
|
1289
1492
|
console.log(`
|
|
1290
1493
|
\u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1291
1494
|
const installResult = await installDependencies(dir);
|
|
@@ -1333,7 +1536,9 @@ async function processImplementJob(ctx, dir, resumed, abort) {
|
|
|
1333
1536
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
1334
1537
|
}
|
|
1335
1538
|
}
|
|
1336
|
-
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, {
|
|
1539
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort, {
|
|
1540
|
+
rebase: !resumed
|
|
1541
|
+
});
|
|
1337
1542
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
1338
1543
|
return {
|
|
1339
1544
|
text: reply,
|
|
@@ -1342,7 +1547,7 @@ async function processImplementJob(ctx, dir, resumed, abort) {
|
|
|
1342
1547
|
...outcome.kind === "pr" ? { pr: outcome.pr } : {}
|
|
1343
1548
|
};
|
|
1344
1549
|
}
|
|
1345
|
-
async function processReviseJob(ctx, dir, resumed, abort) {
|
|
1550
|
+
async function processReviseJob(ctx, dir, resumed, config, abort) {
|
|
1346
1551
|
console.log(`
|
|
1347
1552
|
\u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1348
1553
|
const installResult = await installDependencies(dir);
|
|
@@ -1382,7 +1587,9 @@ async function processReviseJob(ctx, dir, resumed, abort) {
|
|
|
1382
1587
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
1383
1588
|
}
|
|
1384
1589
|
}
|
|
1385
|
-
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, {
|
|
1590
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort, {
|
|
1591
|
+
rebase: !resumed
|
|
1592
|
+
});
|
|
1386
1593
|
if (outcome.kind !== "none") {
|
|
1387
1594
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
1388
1595
|
}
|
|
@@ -1393,11 +1600,11 @@ async function processReviseJob(ctx, dir, resumed, abort) {
|
|
|
1393
1600
|
...result.plans?.length ? { plans: result.plans } : {}
|
|
1394
1601
|
};
|
|
1395
1602
|
}
|
|
1396
|
-
async function processResolveJob(ctx, dir, abort) {
|
|
1603
|
+
async function processResolveJob(ctx, dir, config, abort) {
|
|
1397
1604
|
console.log(`
|
|
1398
1605
|
\u25B6 Resolve ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1399
1606
|
const installResult = await installDependencies(dir);
|
|
1400
|
-
const { resolved, text } = await mergeAndResolveConflicts(ctx, dir, abort);
|
|
1607
|
+
const { resolved, text } = await mergeAndResolveConflicts(ctx, dir, config, abort);
|
|
1401
1608
|
let reply = resolved ? text || "(the agent produced no report)" : `Merged \`${ctx.repo.mergeBranch ?? "the merge branch"}\` into \`${ctx.repo.checkoutBranch}\` cleanly \u2014 there were no conflicts to resolve.`;
|
|
1402
1609
|
if (installResult.status === "failed") {
|
|
1403
1610
|
reply += `
|
|
@@ -1405,14 +1612,14 @@ async function processResolveJob(ctx, dir, abort) {
|
|
|
1405
1612
|
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
1406
1613
|
}
|
|
1407
1614
|
if (abort.signal.aborted) throw new Error("Run canceled by user");
|
|
1408
|
-
await commitWithRepair(ctx, dir, abort);
|
|
1615
|
+
await commitWithRepair(ctx, dir, abort, { skipSocket: true });
|
|
1409
1616
|
await pushBranch(ctx, dir);
|
|
1410
1617
|
const pr = await openPullRequest(ctx);
|
|
1411
1618
|
const outcome = pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
1412
1619
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch });
|
|
1413
1620
|
return { text: reply, widgets: [], ...pr ? { pr } : {} };
|
|
1414
1621
|
}
|
|
1415
|
-
async function processReleaseJob(ctx, dir, resumed, abort) {
|
|
1622
|
+
async function processReleaseJob(ctx, dir, resumed, config, abort) {
|
|
1416
1623
|
console.log(`
|
|
1417
1624
|
\u25B6 Release ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1418
1625
|
const installResult = await installDependencies(dir);
|
|
@@ -1443,7 +1650,9 @@ async function processReleaseJob(ctx, dir, resumed, abort) {
|
|
|
1443
1650
|
);
|
|
1444
1651
|
return { text: reply, widgets: result.widgets };
|
|
1445
1652
|
}
|
|
1446
|
-
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, {
|
|
1653
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, config, abort, {
|
|
1654
|
+
rebase: !resumed
|
|
1655
|
+
});
|
|
1447
1656
|
if (outcome.kind !== "none") {
|
|
1448
1657
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, autoMerged });
|
|
1449
1658
|
}
|
|
@@ -1516,14 +1725,17 @@ async function pollLoop(config) {
|
|
|
1516
1725
|
};
|
|
1517
1726
|
scheduleCancelPoll();
|
|
1518
1727
|
try {
|
|
1519
|
-
|
|
1728
|
+
resetUsage();
|
|
1729
|
+
const { text, widgets, pr, plans, report } = await processJob(ctx, config, abort);
|
|
1730
|
+
const usage = getUsage();
|
|
1520
1731
|
await reportJob(config, ctx.jobId, {
|
|
1521
1732
|
status: "done",
|
|
1522
1733
|
text,
|
|
1523
1734
|
widgets,
|
|
1524
1735
|
pr,
|
|
1525
1736
|
...plans?.length ? { plans } : {},
|
|
1526
|
-
...report ? { report } : {}
|
|
1737
|
+
...report ? { report } : {},
|
|
1738
|
+
...usage.totalTokens > 0 ? { usage } : {}
|
|
1527
1739
|
});
|
|
1528
1740
|
console.log(`\u2713 Job ${ctx.jobId} done`);
|
|
1529
1741
|
} catch (err) {
|