@flumecode/runner 0.16.0 → 0.17.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 CHANGED
@@ -26,8 +26,8 @@ function writeConfig(config) {
26
26
  }
27
27
 
28
28
  // src/run.ts
29
- import { existsSync as existsSync4 } from "node:fs";
30
- import { join as join6 } from "node:path";
29
+ import { existsSync as existsSync3 } from "node:fs";
30
+ import { join as join4 } from "node:path";
31
31
 
32
32
  // src/version.ts
33
33
  import { readFileSync as readFileSync2 } from "node:fs";
@@ -180,1216 +180,1132 @@ async function safeText(res) {
180
180
  }
181
181
  }
182
182
 
183
- // src/plugins/socket.ts
184
- import { exec as execCb } from "node:child_process";
185
- import { readFile as readFile2 } from "node:fs/promises";
186
- import { join as join4 } from "node:path";
187
- import { promisify as promisify2 } from "node:util";
188
-
189
- // src/workspace.ts
190
- import { execFile } from "node:child_process";
191
- import { existsSync as existsSync2 } from "node:fs";
192
- import { mkdtemp, readdir, rm } from "node:fs/promises";
193
- import { tmpdir } from "node:os";
194
- import { join as join2 } from "node:path";
195
- import { promisify } from "node:util";
196
-
197
- // src/types.ts
198
- function jobTitle(ctx) {
199
- return ctx.kind === "init" ? "Initialize FlumeCode wiki" : ctx.request?.title ?? "request";
200
- }
183
+ // src/executor.ts
184
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
185
+ import { query } from "@anthropic-ai/claude-agent-sdk";
201
186
 
202
- // src/logger.ts
203
- var lines = [];
204
- var secrets = [];
205
- var MAX_BYTES = 10 * 1024 * 1024;
206
- function startJobLog(opts) {
207
- lines = [];
208
- secrets = opts.secrets.filter(Boolean);
209
- logEvent("meta", `job ${opts.jobId} (${opts.kind}) started at ${(/* @__PURE__ */ new Date()).toISOString()}`);
210
- }
211
- function redact(s) {
212
- for (const sec of secrets) {
213
- s = s.split(sec).join("***REDACTED***");
214
- }
215
- return s;
187
+ // src/widgets.ts
188
+ import { randomUUID } from "node:crypto";
189
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
190
+ import { z } from "zod";
191
+ var SERVER_NAME = "flume_widgets";
192
+ var SINGLE_SELECT = "single_select";
193
+ var MULTI_SELECT = "multi_select";
194
+ var WIDGET_TOOL_NAMES = [
195
+ `mcp__${SERVER_NAME}__${SINGLE_SELECT}`,
196
+ `mcp__${SERVER_NAME}__${MULTI_SELECT}`
197
+ ];
198
+ var optionsSchema = z.array(z.string().min(1)).min(2).max(8).describe("2\u20138 short, distinct choices for the user to pick from.");
199
+ 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.";
200
+ function createWidgetTooling() {
201
+ const collected = [];
202
+ const singleSelect = tool(
203
+ SINGLE_SELECT,
204
+ "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,
205
+ {
206
+ question: z.string().min(1).describe("The question to ask the user."),
207
+ body: z.string().optional().describe(
208
+ "Optional markdown shown above the question so the user can read the context they're confirming (e.g. the drafted release notes). Omit for plain questions."
209
+ ),
210
+ options: optionsSchema
211
+ },
212
+ async (args) => {
213
+ collected.push({
214
+ id: randomUUID(),
215
+ type: "single_select",
216
+ question: args.question,
217
+ body: args.body,
218
+ options: args.options.map((label) => ({ id: randomUUID(), label })),
219
+ selectedOptionId: null,
220
+ customAnswer: null
221
+ });
222
+ return widgetPosted("single-select");
223
+ }
224
+ );
225
+ const multiSelect = tool(
226
+ MULTI_SELECT,
227
+ "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,
228
+ {
229
+ question: z.string().min(1).describe("The question to ask the user."),
230
+ body: z.string().optional().describe(
231
+ "Optional markdown shown above the question so the user can read the context they're confirming (e.g. the drafted release notes). Omit for plain questions."
232
+ ),
233
+ options: optionsSchema
234
+ },
235
+ async (args) => {
236
+ collected.push({
237
+ id: randomUUID(),
238
+ type: "multi_select",
239
+ question: args.question,
240
+ body: args.body,
241
+ options: args.options.map((label) => ({ id: randomUUID(), label })),
242
+ selectedOptionIds: null,
243
+ customAnswer: null
244
+ });
245
+ return widgetPosted("multi-select");
246
+ }
247
+ );
248
+ const mcpServer = createSdkMcpServer({
249
+ name: SERVER_NAME,
250
+ tools: [singleSelect, multiSelect]
251
+ });
252
+ return { mcpServer, collected };
216
253
  }
217
- function logEvent(section, text) {
218
- lines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${section}] ${redact(text)}`);
254
+ function widgetPosted(kind) {
255
+ return {
256
+ content: [
257
+ {
258
+ type: "text",
259
+ 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.`
260
+ }
261
+ ]
262
+ };
219
263
  }
220
- function getJobLog() {
221
- const full = lines.join("\n");
222
- if (full.length <= MAX_BYTES) return full;
223
- const half = Math.floor(MAX_BYTES / 2);
224
- return full.slice(0, half) + `
225
264
 
226
- \u2026[truncated ${full.length - MAX_BYTES} bytes]\u2026
265
+ // src/plan.ts
266
+ import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
267
+ import { z as z2 } from "zod";
227
268
 
228
- ` + full.slice(-half);
229
- }
269
+ // src/schema-hints.ts
270
+ var INLINE_CODE_HINT = "Wrap code identifiers (function, variable, type, and file names, commands, and flags) in inline backticks, e.g. `getCodingSessionsForRequest`.";
230
271
 
231
- // src/workspace.ts
232
- var exec = promisify(execFile);
233
- var WORKSPACE_PREFIX = "flume-runner-";
234
- var MAX_BUFFER = 1 << 24;
235
- async function git(args) {
236
- logEvent("git", `git ${args.join(" ")}`);
237
- try {
238
- const result = await exec("git", args, { maxBuffer: MAX_BUFFER });
239
- if (result.stdout.trim()) logEvent("git:out", result.stdout.trim());
240
- if (result.stderr.trim()) logEvent("git:err", result.stderr.trim());
241
- return result;
242
- } catch (err) {
243
- logEvent("git:err", String(err.stderr ?? err));
244
- throw err;
245
- }
246
- }
247
- async function ensureGitIdentity(dir, identity) {
248
- await git(["-C", dir, "config", "user.email", identity.email]);
249
- await git(["-C", dir, "config", "user.name", identity.name]);
250
- }
251
- function cloneUrl(ctx) {
252
- const { owner, name, cloneToken } = ctx.repo;
253
- return `https://x-access-token:${cloneToken}@github.com/${owner}/${name}.git`;
254
- }
255
- function detectPackageManager(dir) {
256
- if (!existsSync2(join2(dir, "package.json"))) return null;
257
- if (existsSync2(join2(dir, "pnpm-lock.yaml"))) return "pnpm";
258
- if (existsSync2(join2(dir, "yarn.lock"))) return "yarn";
259
- if (existsSync2(join2(dir, "package-lock.json"))) return "npm";
260
- if (existsSync2(join2(dir, "bun.lockb"))) return "bun";
261
- return "npm";
262
- }
263
- async function installDependencies(dir) {
264
- const manager = detectPackageManager(dir);
265
- if (manager === null) return { status: "skipped" };
266
- const env = { ...process.env, CI: "1", ADBLOCK: "1", DISABLE_OPENCOLLECTIVE: "1" };
267
- logEvent("install", `${manager} install`);
268
- try {
269
- const result = await exec(manager, ["install"], {
270
- cwd: dir,
271
- maxBuffer: MAX_BUFFER,
272
- env,
273
- timeout: 5 * 6e4
274
- });
275
- if (result.stdout.trim()) logEvent("install:out", result.stdout.trim());
276
- if (result.stderr.trim()) logEvent("install:err", result.stderr.trim());
277
- return { status: "installed", manager };
278
- } catch (err) {
279
- const e = err;
280
- const detail = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean).join("\n");
281
- logEvent("install:err", detail || (err instanceof Error ? err.message : String(err)));
282
- return { status: "failed", manager, error: err instanceof Error ? err.message : String(err) };
283
- }
284
- }
285
- async function makeWorkspace() {
286
- return mkdtemp(join2(tmpdir(), WORKSPACE_PREFIX));
272
+ // src/plan.ts
273
+ var SERVER_NAME2 = "flume_plan";
274
+ var SUBMIT_PLAN = "submit_plan";
275
+ var PLAN_TOOL_NAME = `mcp__${SERVER_NAME2}__${SUBMIT_PLAN}`;
276
+ var PLAN_MARKER = "<!-- flumecode:end-of-plan -->";
277
+ var pseudoCodeEntrySchema = z2.object({
278
+ file: z2.string().min(1),
279
+ pseudoCode: z2.string().min(1)
280
+ });
281
+ var stepSchema = z2.object({
282
+ title: z2.string().min(1).describe("A concise imperative title for this step."),
283
+ description: z2.array(z2.string().min(1)).min(1).describe(
284
+ "Bullet points that explain this step's change so a reviewer can judge whether the design is correct. Each array item is one short, self-contained bullet \u2014 not a single paragraph, and not a restatement of the pseudo code. " + INLINE_CODE_HINT
285
+ ),
286
+ pseudoCode: z2.array(pseudoCodeEntrySchema).optional().describe(
287
+ "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."
288
+ )
289
+ });
290
+ var planInputSchema = {
291
+ title: z2.string().min(1).max(120).describe(
292
+ "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."
293
+ ),
294
+ scope: z2.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
295
+ goal: z2.string().min(1).describe("One or two sentences stating the outcome. " + INLINE_CODE_HINT),
296
+ rootCause: z2.string().optional().describe(
297
+ 'For bug fixes (scope === "fix"): the underlying cause of the bug \u2014 the specific code, logic, or condition that produces the incorrect behavior, not just the symptom. Required when scope is "fix"; omit for all other scopes. ' + INLINE_CODE_HINT
298
+ ),
299
+ assumptions: z2.array(z2.string()).describe("Anything decided during planning, including unanswered defaults."),
300
+ requirements: z2.array(z2.string().min(1)).min(1).describe(
301
+ "Required, human-readable statements of what this change must accomplish and why, in plain language a non-technical reader can follow. Distinct from acceptanceCriteria: requirements explain intent/rationale; acceptance criteria are the machine-checkable proof. At least 1 required. " + INLINE_CODE_HINT
302
+ ),
303
+ steps: z2.array(stepSchema).min(1).describe("Ordered list of changes. Each step says what and why, with file references."),
304
+ acceptanceCriteria: z2.array(z2.string().min(1)).min(2).describe(
305
+ "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. " + INLINE_CODE_HINT
306
+ ),
307
+ risks: z2.array(z2.string()).describe("Anything that could change the approach."),
308
+ outOfScope: z2.array(z2.string()).describe("What is deliberately not being done.")
309
+ };
310
+ function requireRootCauseForFix(schema) {
311
+ return schema.superRefine((plan, ctx) => {
312
+ if (plan.scope === "fix" && (plan.rootCause === void 0 || plan.rootCause.trim() === "")) {
313
+ ctx.addIssue({
314
+ code: z2.ZodIssueCode.custom,
315
+ path: ["rootCause"],
316
+ message: 'rootCause is required and must be non-empty when scope is "fix".'
317
+ });
318
+ }
319
+ });
287
320
  }
288
- var MAX_WORKSPACES = 8;
289
- var workspaceRegistry = /* @__PURE__ */ new Map();
290
- async function acquireWorkspace(key) {
291
- const existing = workspaceRegistry.get(key);
292
- if (existing !== void 0 && existsSync2(existing)) {
293
- workspaceRegistry.delete(key);
294
- workspaceRegistry.set(key, existing);
295
- return { dir: existing, reused: true };
321
+ var planSchema = requireRootCauseForFix(z2.object(planInputSchema));
322
+ function renderPlan(plan) {
323
+ const lines2 = [];
324
+ lines2.push(`# ${plan.title}`);
325
+ lines2.push("");
326
+ lines2.push(`**Scope** \u2014 \`${plan.scope}\``);
327
+ lines2.push("");
328
+ lines2.push(`**Goal** \u2014 ${plan.goal}`);
329
+ if (plan.assumptions.length > 0) {
330
+ lines2.push("");
331
+ lines2.push("**Assumptions**");
332
+ for (const assumption of plan.assumptions) {
333
+ lines2.push(`- ${assumption}`);
334
+ }
296
335
  }
297
- const dir = await makeWorkspace();
298
- workspaceRegistry.set(key, dir);
299
- if (workspaceRegistry.size > MAX_WORKSPACES) {
300
- const oldest = workspaceRegistry.keys().next().value;
301
- const oldDir = workspaceRegistry.get(oldest);
302
- workspaceRegistry.delete(oldest);
303
- rm(oldDir, { recursive: true, force: true }).catch(() => {
304
- });
336
+ if (plan.rootCause && plan.rootCause.trim().length > 0) {
337
+ lines2.push("");
338
+ lines2.push("## Root cause");
339
+ lines2.push(plan.rootCause);
305
340
  }
306
- return { dir, reused: false };
307
- }
308
- async function discardWorkspace(key) {
309
- const dir = workspaceRegistry.get(key);
310
- workspaceRegistry.delete(key);
311
- if (dir !== void 0) {
312
- await cleanup(dir).catch(() => {
313
- });
341
+ lines2.push("");
342
+ lines2.push("## Requirements");
343
+ for (const requirement of plan.requirements) {
344
+ lines2.push(`- ${requirement}`);
314
345
  }
315
- }
316
- async function resetWorkspace(dir) {
317
- await git(["-C", dir, "reset", "--hard", "HEAD"]).catch(() => {
318
- });
319
- await git(["-C", dir, "clean", "-fd"]).catch(() => {
320
- });
321
- }
322
- async function prepareAtSha(ctx, dir, reused) {
323
- const identity = { name: ctx.agentName, email: ctx.agentEmail };
324
- if (!reused) {
325
- await cloneAtSha(ctx, dir);
326
- await ensureGitIdentity(dir, identity);
327
- return;
346
+ lines2.push("");
347
+ lines2.push("## Steps");
348
+ for (const [i, step] of plan.steps.entries()) {
349
+ lines2.push("");
350
+ lines2.push(`### ${i + 1}. ${step.title}`);
351
+ lines2.push("");
352
+ for (const bullet of step.description) {
353
+ lines2.push(`- ${bullet}`);
354
+ }
355
+ if (step.pseudoCode && step.pseudoCode.length > 0) {
356
+ for (const entry of step.pseudoCode) {
357
+ lines2.push("");
358
+ lines2.push(`\`${entry.file}\``);
359
+ lines2.push("");
360
+ lines2.push("```");
361
+ lines2.push(entry.pseudoCode);
362
+ lines2.push("```");
363
+ }
364
+ }
328
365
  }
329
- await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
330
- await ensureGitIdentity(dir, identity);
331
- }
332
- async function prepareResumingBranch(ctx, dir, reused) {
333
- const identity = { name: ctx.agentName, email: ctx.agentEmail };
334
- if (!reused) {
335
- const result = await cloneResumingBranch(ctx, dir);
336
- await ensureGitIdentity(dir, identity);
337
- return result;
366
+ lines2.push("");
367
+ lines2.push("## Acceptance criteria");
368
+ for (const criterion of plan.acceptanceCriteria) {
369
+ lines2.push(`- [ ] ${criterion}`);
338
370
  }
339
- await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
340
- await ensureGitIdentity(dir, identity);
341
- return { resumed: true };
342
- }
343
- async function sweepWorkspaces() {
344
- const base = tmpdir();
345
- let entries;
346
- try {
347
- entries = await readdir(base);
348
- } catch {
349
- return 0;
371
+ if (plan.risks.length > 0) {
372
+ lines2.push("");
373
+ lines2.push("**Risks / open questions**");
374
+ for (const risk of plan.risks) {
375
+ lines2.push(`- ${risk}`);
376
+ }
350
377
  }
351
- let removed = 0;
352
- for (const entry of entries) {
353
- if (!entry.startsWith(WORKSPACE_PREFIX)) continue;
354
- try {
355
- await rm(join2(base, entry), { recursive: true, force: true });
356
- removed++;
357
- } catch {
378
+ if (plan.outOfScope.length > 0) {
379
+ lines2.push("");
380
+ lines2.push("**Out of scope**");
381
+ for (const item of plan.outOfScope) {
382
+ lines2.push(`- ${item}`);
358
383
  }
359
384
  }
360
- return removed;
385
+ lines2.push("");
386
+ lines2.push(PLAN_MARKER);
387
+ return lines2.join("\n");
361
388
  }
362
- async function cloneAtSha(ctx, dir) {
363
- await git(["clone", "--quiet", cloneUrl(ctx), dir]);
364
- await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
389
+ var submitPlanInputSchema = {
390
+ plans: z2.array(requireRootCauseForFix(z2.object(planInputSchema))).min(1).refine(
391
+ (arr) => {
392
+ const titles = arr.map((p) => p.title.trim()).filter((t) => t.length > 0);
393
+ return new Set(titles).size === titles.length;
394
+ },
395
+ { message: "Each plan must have a distinct non-empty title" }
396
+ )
397
+ };
398
+ var submitPlanSchema = z2.object(submitPlanInputSchema);
399
+ function createPlanTooling() {
400
+ let renderedPlans = null;
401
+ const submitPlan = tool2(
402
+ SUBMIT_PLAN,
403
+ `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. requirements is required in each plan: at least 1 plain-language statement of what the change must accomplish and why (human-readable intent), separate from the machine-checkable acceptanceCriteria. When a plan's scope is "fix", rootCause is required: a non-empty explanation of the underlying cause of the bug (not just the symptom). `,
404
+ submitPlanInputSchema,
405
+ async (args) => {
406
+ const parsed = submitPlanSchema.parse(args);
407
+ renderedPlans = parsed.plans.map(renderPlan);
408
+ return {
409
+ content: [
410
+ {
411
+ type: "text",
412
+ text: "Plan(s) submitted. The runner will render and post them as your comment(s). End your turn now."
413
+ }
414
+ ]
415
+ };
416
+ }
417
+ );
418
+ const mcpServer = createSdkMcpServer2({
419
+ name: SERVER_NAME2,
420
+ tools: [submitPlan]
421
+ });
422
+ return { mcpServer, getPlans: () => renderedPlans };
365
423
  }
366
- async function cloneResumingBranch(ctx, dir) {
367
- await git(["clone", "--quiet", cloneUrl(ctx), dir]);
368
- try {
369
- await git(["-C", dir, "fetch", "--quiet", "origin", ctx.repo.checkoutBranch]);
370
- await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, "FETCH_HEAD"]);
371
- return { resumed: true };
372
- } catch {
373
- await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
374
- return { resumed: false };
424
+ function countPlanAcceptanceCriteria(planBody) {
425
+ if (!planBody) return 0;
426
+ const lines2 = planBody.split("\n");
427
+ const start2 = lines2.findIndex((l) => l.trim() === "## Acceptance criteria");
428
+ if (start2 === -1) return 0;
429
+ let count = 0;
430
+ for (let i = start2 + 1; i < lines2.length; i++) {
431
+ const line = lines2[i] ?? "";
432
+ if (line.startsWith("## ")) break;
433
+ if (line.startsWith("- [ ] ")) count++;
375
434
  }
435
+ return count;
376
436
  }
377
- async function hasChanges(dir) {
378
- await git(["-C", dir, "add", "-A"]);
379
- const { stdout: stdout2 } = await git(["-C", dir, "status", "--porcelain"]);
380
- return stdout2.trim().length > 0;
381
- }
382
- async function gitDiffStat(dir) {
383
- const { stdout: stdout2 } = await git(["-C", dir, "--no-pager", "diff", "--stat"]);
384
- return stdout2;
385
- }
386
- var PreCommitError = class extends Error {
387
- constructor(log) {
388
- super("pre-commit checks failed");
389
- this.log = log;
390
- this.name = "PreCommitError";
391
- }
437
+
438
+ // src/report.ts
439
+ import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
440
+ import { z as z3 } from "zod";
441
+ var SERVER_NAME3 = "flume_report";
442
+ var SUBMIT_REPORT = "submit_report";
443
+ var REPORT_TOOL_NAME = `mcp__${SERVER_NAME3}__${SUBMIT_REPORT}`;
444
+ var STATUS_ICON = {
445
+ met: "\u2705",
446
+ not_met: "\u274C",
447
+ unclear: "\u26A0\uFE0F"
392
448
  };
393
- function commitFailureLog(err) {
394
- const e = err;
395
- const parts = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter((s) => s.length > 0);
396
- return parts.length > 0 ? parts.join("\n") : e.message ?? String(err);
397
- }
398
- function isUnsupportedGitSubcommand(err) {
399
- const e = err;
400
- const text = `${typeof e.stderr === "string" ? e.stderr : ""}
401
- ${e.message ?? ""}`;
402
- return /is not a git command|unknown subcommand|usage: git hook/i.test(text);
403
- }
404
- async function runRepoChecks(dir) {
405
- try {
406
- await git(["-C", dir, "hook", "run", "pre-commit"]);
407
- logEvent("checks", "pre-commit hook passed");
408
- return { ok: true, log: "", skipped: false };
409
- } catch (err) {
410
- if (isUnsupportedGitSubcommand(err)) {
411
- logEvent("checks", "pre-commit hook skipped (git too old)");
412
- return { ok: true, log: "", skipped: true };
449
+ var evidenceSchema = z3.object({
450
+ file: z3.string().min(1).describe("Repo-relative path the hunk comes from."),
451
+ hunk: z3.string().min(1).describe(
452
+ "A unified-diff hunk proving the criterion \u2014 the lines that matter, not the whole file. MUST keep the `@@ -a,b +c,d @@` hunk header line(s) exactly as they appear in `git --no-pager diff`; the report renders file line numbers from them. Rendered verbatim as a ```diff block."
453
+ ),
454
+ note: z3.string().optional().describe("Optional one-line explanation of why this hunk satisfies the criterion.")
455
+ });
456
+ var acVerdictSchema = z3.object({
457
+ criterion: z3.string().min(1).describe("The acceptance-criterion text, verbatim from the plan."),
458
+ status: z3.enum(["met", "not_met", "unclear"]).describe("Verdict for this criterion, verified against the actual diff."),
459
+ rationale: z3.string().min(1).describe("One or two sentences on why the verdict holds. " + INLINE_CODE_HINT),
460
+ evidence: z3.array(evidenceSchema).describe(
461
+ "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."
462
+ )
463
+ });
464
+ var reportInputSchema = {
465
+ summary: z3.string().min(1).describe("One or two sentences on what was implemented. " + INLINE_CODE_HINT),
466
+ filesChanged: z3.string().min(1).describe(
467
+ "Markdown: the list of files changed (from the diff). Rendered under '## Files changed'."
468
+ ),
469
+ codeQuality: z3.string().min(1).describe(
470
+ "Markdown: the code-quality review outcome and anything left as nice-to-have. Rendered under '## Code quality'. " + INLINE_CODE_HINT
471
+ ),
472
+ caveats: z3.string().min(1).describe(
473
+ "Markdown: anything deferred, unmet, or worth a human's eyes, incl. diff hunks that map to no plan AC. Write 'None.' if nothing. Rendered under '## Caveats / follow-ups'. " + INLINE_CODE_HINT
474
+ ),
475
+ acceptanceCriteria: z3.array(acVerdictSchema).describe(
476
+ "One entry per acceptance criterion from the plan, in plan order, each with a verdict and the diff evidence behind it. May be empty for resolve runs (no plan to verify)."
477
+ ),
478
+ conflictResolution: z3.string().optional().describe(
479
+ "Markdown: present ONLY when a merge conflict was actually resolved. Explain, per conflicted file, how ours/theirs were integrated. Rendered under '## Conflict resolution'. Omit entirely when no conflict occurred."
480
+ )
481
+ };
482
+ var reportSchema = z3.object(reportInputSchema);
483
+ function renderReport(report) {
484
+ const lines2 = [];
485
+ lines2.push(report.summary.trim());
486
+ lines2.push("", "## Files changed", "", report.filesChanged.trim());
487
+ if (report.acceptanceCriteria.length > 0) {
488
+ lines2.push("", "## Acceptance criteria");
489
+ for (const ac of report.acceptanceCriteria) {
490
+ lines2.push("");
491
+ lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
492
+ lines2.push("");
493
+ lines2.push(ac.rationale.trim());
494
+ for (const ev of ac.evidence) {
495
+ lines2.push("");
496
+ lines2.push(ev.note ? `\`${ev.file}\` \u2014 ${ev.note}` : `\`${ev.file}\``);
497
+ lines2.push("");
498
+ lines2.push("```diff");
499
+ lines2.push(ev.hunk.replace(/\n+$/, ""));
500
+ lines2.push("```");
501
+ }
413
502
  }
414
- const log = commitFailureLog(err);
415
- logEvent("checks:err", log);
416
- return { ok: false, log, skipped: false };
417
503
  }
418
- }
419
- async function commitChanges(ctx, dir) {
420
- if (!await hasChanges(dir)) return false;
421
- try {
422
- await git(["-C", dir, "commit", "--quiet", "-m", `FlumeCode: ${jobTitle(ctx)}`]);
423
- } catch (err) {
424
- throw new PreCommitError(commitFailureLog(err));
504
+ if (report.conflictResolution?.trim()) {
505
+ lines2.push("", "## Conflict resolution", "", report.conflictResolution.trim());
425
506
  }
426
- return true;
427
- }
428
- async function pushBranch(ctx, dir) {
429
- await git(["-C", dir, "push", "--quiet", "-u", "origin", ctx.repo.checkoutBranch]);
507
+ lines2.push("", "## Code quality", "", report.codeQuality.trim());
508
+ lines2.push("", "## Caveats / follow-ups", "", report.caveats.trim());
509
+ return lines2.join("\n");
430
510
  }
431
- var RebaseConflictError = class extends Error {
432
- constructor(mergeBranch, files) {
433
- const list = files.length ? `: ${files.join(", ")}` : "";
434
- super(`Rebase onto ${mergeBranch} hit conflicts in ${files.length} file(s)${list}`);
435
- this.mergeBranch = mergeBranch;
436
- this.files = files;
437
- this.name = "RebaseConflictError";
438
- }
439
- };
440
- async function rebaseOntoMergeBranch(ctx, dir) {
441
- const { mergeBranch } = ctx.repo;
442
- if (!mergeBranch) return;
443
- await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
444
- try {
445
- await git(["-C", dir, "rebase", "--empty=drop", "FETCH_HEAD"]);
446
- } catch (err) {
447
- const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
448
- () => ({ stdout: "" })
449
- );
450
- const files = conflicted.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
451
- await git(["-C", dir, "rebase", "--abort"]).catch(() => {
452
- });
453
- if (files.length === 0) throw err;
454
- throw new RebaseConflictError(mergeBranch, files);
455
- }
511
+ function createReportTooling() {
512
+ let submittedReport = null;
513
+ const submitReport = tool3(
514
+ SUBMIT_REPORT,
515
+ "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`, `filesChanged`, `codeQuality`, and `caveats` are the four named markdown sections. Do NOT include a PR link \u2014 the runner appends it.",
516
+ reportInputSchema,
517
+ async (args) => {
518
+ submittedReport = reportSchema.parse(args);
519
+ return {
520
+ content: [
521
+ {
522
+ type: "text",
523
+ text: "Report submitted. The runner will render and post it. End your turn now."
524
+ }
525
+ ]
526
+ };
527
+ }
528
+ );
529
+ const mcpServer = createSdkMcpServer3({
530
+ name: SERVER_NAME3,
531
+ tools: [submitReport]
532
+ });
533
+ return { mcpServer, getReport: () => submittedReport };
456
534
  }
457
- async function mergeInMergeBranch(ctx, dir) {
458
- const { mergeBranch } = ctx.repo;
459
- if (!mergeBranch) return { conflicted: false };
460
- await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
461
- try {
462
- await git(["-C", dir, "merge", "--no-edit", "FETCH_HEAD"]);
463
- return { conflicted: false };
464
- } catch {
465
- return { conflicted: true };
535
+
536
+ // src/logger.ts
537
+ var lines = [];
538
+ var secrets = [];
539
+ var MAX_BYTES = 10 * 1024 * 1024;
540
+ function startJobLog(opts) {
541
+ lines = [];
542
+ secrets = opts.secrets.filter(Boolean);
543
+ logEvent("meta", `job ${opts.jobId} (${opts.kind}) started at ${(/* @__PURE__ */ new Date()).toISOString()}`);
544
+ }
545
+ function redact(s) {
546
+ for (const sec of secrets) {
547
+ s = s.split(sec).join("***REDACTED***");
466
548
  }
549
+ return s;
467
550
  }
468
- async function listUnmergedPaths(dir) {
469
- const { stdout: stdout2 } = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(() => ({
470
- stdout: ""
471
- }));
472
- return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
551
+ function logEvent(section, text) {
552
+ lines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${section}] ${redact(text)}`);
473
553
  }
474
- async function listConflictMarkerPaths(dir, paths) {
475
- if (paths.length === 0) return [];
476
- const { stdout: stdout2 } = await git([
477
- "-C",
478
- dir,
479
- "grep",
480
- "--no-color",
481
- "-lE",
482
- "^(<<<<<<<|>>>>>>>|\\|\\|\\|\\|\\|\\|\\|)",
483
- "--",
484
- ...paths
485
- ]).catch(() => ({ stdout: "" }));
486
- return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
554
+ function getJobLog() {
555
+ const full = lines.join("\n");
556
+ if (full.length <= MAX_BYTES) return full;
557
+ const half = Math.floor(MAX_BYTES / 2);
558
+ return full.slice(0, half) + `
559
+
560
+ \u2026[truncated ${full.length - MAX_BYTES} bytes]\u2026
561
+
562
+ ` + full.slice(-half);
487
563
  }
488
- async function openPullRequest(ctx) {
489
- const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
490
- if (!mergeBranch) return null;
491
- const apiBase = `https://api.github.com/repos/${owner}/${name}`;
492
- const headers = {
493
- authorization: `Bearer ${cloneToken}`,
494
- accept: "application/vnd.github+json",
495
- "x-github-api-version": "2022-11-28",
496
- "content-type": "application/json"
564
+
565
+ // src/executor.ts
566
+ var FLUME_PLUGIN_DIR = fileURLToPath2(new URL("../skills-plugin", import.meta.url));
567
+ function emptyUsage() {
568
+ return {
569
+ inputTokens: 0,
570
+ outputTokens: 0,
571
+ cacheCreationTokens: 0,
572
+ cacheReadTokens: 0,
573
+ costUsd: 0
497
574
  };
498
- const title = jobTitle(ctx);
499
- 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}".`;
500
- const res = await fetch(`${apiBase}/pulls`, {
501
- method: "POST",
502
- headers,
503
- body: JSON.stringify({
504
- title: `FlumeCode: ${title}`,
505
- head: checkoutBranch,
506
- base: mergeBranch,
507
- body
508
- })
509
- });
510
- if (res.status === 201) {
511
- const data = await res.json();
512
- return { number: data.number, url: data.html_url };
513
- }
514
- if (res.status === 422) {
515
- const list = await fetch(
516
- `${apiBase}/pulls?state=open&head=${owner}:${checkoutBranch}&base=${mergeBranch}`,
517
- { headers }
518
- );
519
- if (list.ok) {
520
- const open = await list.json();
521
- if (open[0]) return { number: open[0].number, url: open[0].html_url };
522
- }
523
- return null;
524
- }
525
- throw new Error(`PR creation failed: ${res.status} ${await res.text()}`);
526
575
  }
527
- async function cleanup(dir) {
528
- await rm(dir, { recursive: true, force: true });
576
+ var usageAcc = emptyUsage();
577
+ function resetUsage() {
578
+ usageAcc = emptyUsage();
529
579
  }
530
- function parsePrFromSubject(subject) {
531
- const m = subject.match(/\(#(\d+)\)\s*$/);
532
- return m ? Number(m[1]) : null;
580
+ function getUsage() {
581
+ const totalTokens = usageAcc.inputTokens + usageAcc.outputTokens + usageAcc.cacheCreationTokens + usageAcc.cacheReadTokens;
582
+ return { ...usageAcc, totalTokens };
533
583
  }
534
- async function incomingPrNumbers(ctx, dir, paths) {
535
- if (!paths.length) return [];
536
- try {
537
- const mergeHeadResult = await git(["-C", dir, "rev-parse", "MERGE_HEAD"]);
538
- const mergeHead = mergeHeadResult.stdout.trim();
539
- const baseResult = await git(["-C", dir, "merge-base", "HEAD", mergeHead]);
540
- const base = baseResult.stdout.trim();
541
- const logResult = await git([
542
- "-C",
543
- dir,
544
- "log",
545
- "--no-merges",
546
- `--format=%H%x1f%s`,
547
- `${base}..${mergeHead}`,
548
- "--",
549
- ...paths
550
- ]);
551
- const nums = /* @__PURE__ */ new Set();
552
- const needLookup = [];
553
- for (const line of logResult.stdout.split("\n").filter(Boolean)) {
554
- const idx = line.indexOf("");
555
- const sha = line.slice(0, idx);
556
- const subject = line.slice(idx + 1);
557
- const n = parsePrFromSubject(subject);
558
- if (n !== null) nums.add(n);
559
- else needLookup.push(sha);
584
+ function stringifyResult(content) {
585
+ if (typeof content === "string") return content;
586
+ if (Array.isArray(content)) {
587
+ return content.map(
588
+ (c) => typeof c === "object" && c !== null && "text" in c ? String(c.text) : JSON.stringify(c)
589
+ ).join("\n");
590
+ }
591
+ return JSON.stringify(content);
592
+ }
593
+ async function runClaudeCode(opts) {
594
+ let finalText = "";
595
+ const { mcpServer, collected } = createWidgetTooling();
596
+ const { mcpServer: planServer, getPlans } = createPlanTooling();
597
+ const { mcpServer: reportServer, getReport } = createReportTooling();
598
+ for await (const message of query({
599
+ prompt: opts.prompt,
600
+ options: {
601
+ cwd: opts.cwd,
602
+ permissionMode: opts.permissionMode,
603
+ allowDangerouslySkipPermissions: opts.permissionMode === "bypassPermissions",
604
+ ...opts.model ? { model: opts.model } : {},
605
+ ...opts.abortController ? { abortController: opts.abortController } : {},
606
+ maxTurns: opts.maxTurns ?? 40,
607
+ // Hermetic: ignore ALL config from the checked-out repo (CLAUDE.md,
608
+ // .claude/settings.json, .claude/skills, ) and the runner owner's
609
+ // ~/.claude. Only our plugin's skills are exposed to the agent. Auto-memory
610
+ // is disabled separately via CLAUDE_CODE_DISABLE_AUTO_MEMORY (see cli.ts).
611
+ settingSources: [],
612
+ plugins: [{ type: "local", path: FLUME_PLUGIN_DIR }],
613
+ // Pre-approve the widget, plan, and Task tools so they run without a
614
+ // permission prompt in headless mode (allow-listing pre-approves them; it
615
+ // does NOT restrict anything else). Task lets the implement-plan
616
+ // orchestrator spawn its subagents; without pre-approval the spawn could
617
+ // stall waiting for an approval no one can give.
618
+ mcpServers: { flume_widgets: mcpServer, flume_plan: planServer, flume_report: reportServer },
619
+ allowedTools: [...WIDGET_TOOL_NAMES, PLAN_TOOL_NAME, REPORT_TOOL_NAME, "Task"]
560
620
  }
561
- for (const sha of needLookup) {
562
- for (const n of await prNumbersForCommit(ctx, sha)) nums.add(n);
621
+ })) {
622
+ if (message.type === "assistant") {
623
+ const content = message.message?.content;
624
+ if (Array.isArray(content)) {
625
+ for (const block of content) {
626
+ if (block && block.type === "text" && typeof block.text === "string") {
627
+ process.stdout.write(block.text);
628
+ logEvent("agent", block.text);
629
+ } else if (block && block.type === "tool_use") {
630
+ logEvent("tool_use", `${block.name} ${JSON.stringify(block.input)}`);
631
+ }
632
+ }
633
+ }
634
+ } else if (message.type === "user") {
635
+ const content = message.message?.content;
636
+ if (Array.isArray(content)) {
637
+ for (const block of content) {
638
+ if (block && block.type === "tool_result") {
639
+ logEvent("tool_result", stringifyResult(block.content));
640
+ }
641
+ }
642
+ }
643
+ } else if (message.type === "result") {
644
+ finalText = message.result ?? "";
645
+ logEvent("result", finalText);
646
+ const resultMsg = message;
647
+ if (resultMsg.usage) {
648
+ usageAcc.inputTokens += resultMsg.usage.input_tokens ?? 0;
649
+ usageAcc.outputTokens += resultMsg.usage.output_tokens ?? 0;
650
+ usageAcc.cacheCreationTokens += resultMsg.usage.cache_creation_input_tokens ?? 0;
651
+ usageAcc.cacheReadTokens += resultMsg.usage.cache_read_input_tokens ?? 0;
652
+ }
653
+ usageAcc.costUsd += resultMsg.total_cost_usd ?? 0;
654
+ } else if (message.type === "system") {
655
+ logEvent("system", JSON.stringify(message));
563
656
  }
564
- return [...nums];
565
- } catch {
566
- return [];
567
657
  }
658
+ process.stdout.write("\n");
659
+ if (opts.abortController?.signal.aborted) {
660
+ throw new Error("Run canceled by user");
661
+ }
662
+ return { text: finalText, widgets: collected, plans: getPlans(), report: getReport() };
568
663
  }
569
- async function prNumbersForCommit(ctx, sha) {
570
- const { owner, name, cloneToken } = ctx.repo;
664
+
665
+ // src/health.ts
666
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
667
+ var PROBE_TIMEOUT_MS = 6e4;
668
+ async function checkClaudeCode() {
571
669
  try {
572
- const res = await fetch(`https://api.github.com/repos/${owner}/${name}/commits/${sha}/pulls`, {
573
- headers: {
574
- authorization: `Bearer ${cloneToken}`,
575
- accept: "application/vnd.github+json",
576
- "x-github-api-version": "2022-11-28"
670
+ let sawResult = false;
671
+ const run = (async () => {
672
+ for await (const message of query2({
673
+ prompt: "Reply with the single word: ok",
674
+ options: { permissionMode: "bypassPermissions", maxTurns: 1 }
675
+ })) {
676
+ if (message.type === "result") {
677
+ sawResult = true;
678
+ const isError = message.is_error === true;
679
+ const subtype = message.subtype;
680
+ if (isError)
681
+ throw new Error(
682
+ subtype ? `Claude Code error: ${subtype}` : "Claude Code returned an error"
683
+ );
684
+ }
577
685
  }
578
- });
579
- if (!res.ok) return [];
580
- return (await res.json()).map((p) => p.number);
581
- } catch {
582
- return [];
686
+ })();
687
+ await withTimeout(run, PROBE_TIMEOUT_MS, "Claude Code did not respond in time");
688
+ if (!sawResult) return { ready: false, error: "Claude Code produced no result" };
689
+ return { ready: true };
690
+ } catch (err) {
691
+ return { ready: false, error: errorMessage(err) };
583
692
  }
584
693
  }
694
+ function withTimeout(p, ms, message) {
695
+ return new Promise((resolve, reject) => {
696
+ const timer = setTimeout(() => reject(new Error(message)), ms);
697
+ p.then(
698
+ (v) => {
699
+ clearTimeout(timer);
700
+ resolve(v);
701
+ },
702
+ (e) => {
703
+ clearTimeout(timer);
704
+ reject(e);
705
+ }
706
+ );
707
+ });
708
+ }
709
+ function errorMessage(err) {
710
+ return err instanceof Error ? err.message : String(err);
711
+ }
585
712
 
586
- // src/plugins/manifest.ts
587
- import { existsSync as existsSync3 } from "node:fs";
588
- import { readdir as readdir2, readFile } from "node:fs/promises";
589
- import { join as join3 } from "node:path";
590
- async function loadPlugins(dir) {
591
- const pluginsDir = join3(dir, ".flumecode", "plugins");
592
- if (!existsSync3(pluginsDir)) return [];
593
- let entries;
594
- try {
595
- entries = await readdir2(pluginsDir);
596
- } catch {
597
- return [];
598
- }
599
- const manifests = [];
600
- for (const entry of entries) {
601
- const manifestPath = join3(pluginsDir, entry, "plugin.json");
602
- try {
603
- const raw = JSON.parse(await readFile(manifestPath, "utf8"));
604
- const manifest = parseManifest(raw);
605
- if (manifest) manifests.push(manifest);
606
- } catch {
607
- }
608
- }
609
- return manifests;
713
+ // src/rules.ts
714
+ import { readFileSync as readFileSync3 } from "node:fs";
715
+ import { join as join2 } from "node:path";
716
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
717
+ var RULES_DIR = fileURLToPath3(new URL("../skills-plugin/rules", import.meta.url));
718
+ function loadRule(name) {
719
+ const raw = readFileSync3(join2(RULES_DIR, `${name}.md`), "utf8");
720
+ return stripFrontMatter(raw).trim();
610
721
  }
611
- function parseManifest(raw) {
612
- if (typeof raw !== "object" || raw === null) return null;
613
- const r = raw;
614
- if (typeof r.key !== "string" || !r.key) return null;
615
- if (r.socket !== "pre-commit") return null;
616
- if (typeof r.run !== "string" || !r.run) return null;
617
- let report;
618
- const rep = r.report;
619
- if (rep && typeof rep.file === "string" && rep.file && rep.format === "jest") {
620
- report = { file: rep.file, format: "jest" };
621
- }
622
- return { key: r.key, socket: r.socket, run: r.run, ...report ? { report } : {} };
722
+ function stripFrontMatter(raw) {
723
+ const match = raw.match(/^---\n.*?\n---\n/s);
724
+ return match ? raw.slice(match[0].length) : raw;
623
725
  }
624
726
 
625
- // src/plugins/socket.ts
626
- var exec2 = promisify2(execCb);
627
- var MAX_OUTPUT = 8 * 1024;
628
- function cap(s) {
629
- return s.length <= MAX_OUTPUT ? s : s.slice(s.length - MAX_OUTPUT);
630
- }
631
- var lastSocketResults = [];
632
- function resetSocketResults() {
633
- lastSocketResults = [];
634
- }
635
- function getSocketResults() {
636
- return lastSocketResults;
637
- }
638
- async function runSocket(socketName, dir) {
639
- const plugins = (await loadPlugins(dir)).filter((p) => p.socket === socketName);
640
- const results = [];
641
- for (const plugin of plugins) {
642
- const result = await runPluginCommand(plugin.run, dir);
643
- const metrics = await readMetrics(plugin.report, dir);
644
- if (result.exitCode !== 0) {
645
- results.push({
646
- key: plugin.key,
647
- status: "failed",
648
- output: cap(result.output),
649
- ...metrics ? { metrics } : {}
650
- });
651
- lastSocketResults = results;
652
- throw new PreCommitError(`[plugin:${plugin.key}] ${result.output}`);
653
- }
654
- results.push({
655
- key: plugin.key,
656
- status: "passed",
657
- output: cap(result.output),
658
- ...metrics ? { metrics } : {}
659
- });
660
- }
661
- lastSocketResults = results;
727
+ // src/prompt.ts
728
+ function appendRule(lines2, intro, ruleName) {
729
+ lines2.push("", intro, "", loadRule(ruleName));
662
730
  }
663
- async function readMetrics(report, dir) {
664
- if (!report) return void 0;
665
- try {
666
- const raw = JSON.parse(await readFile2(join4(dir, report.file), "utf8"));
667
- if (report.format === "jest") {
668
- return {
669
- testsRun: Number(raw.numTotalTests) || 0,
670
- testsFailed: Number(raw.numFailedTests) || 0
671
- };
672
- }
673
- } catch {
731
+ var WRITING_INTRO = "These technical-writing guidelines apply to the plan and report prose you author in this run:";
732
+ function turnHeading(turn, agentName) {
733
+ if (turn.role === "user") return "User";
734
+ if (turn.failed) return `${agentName} (this run ended in an error)`;
735
+ if (turn.kind === "plan") return `${agentName} (proposed a plan)`;
736
+ if (turn.kind === "report") return `${agentName} (implementation report)`;
737
+ return agentName;
738
+ }
739
+ function appendThread(lines2, ctx) {
740
+ if (!ctx.thread || ctx.thread.length === 0) return;
741
+ lines2.push("", "# Conversation so far");
742
+ for (const turn of ctx.thread) {
743
+ lines2.push("", `## ${turnHeading(turn, ctx.agentName)}`, turn.content);
674
744
  }
675
- return void 0;
676
745
  }
677
- async function runPluginCommand(command2, cwd) {
678
- try {
679
- const result = await exec2(command2, { cwd, maxBuffer: 1 << 24 });
680
- const output = [result.stdout, result.stderr].map((s) => s.trim()).filter(Boolean).join("\n");
681
- return { exitCode: 0, output };
682
- } catch (err) {
683
- const e = err;
684
- const output = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean).join("\n");
685
- return { exitCode: typeof e.code === "number" ? e.code : 1, output };
746
+ function buildPrompt(ctx) {
747
+ 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.`;
748
+ 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.`;
749
+ 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.`;
750
+ const lines2 = [
751
+ `You are "${ctx.agentName}", an autonomous coding agent working inside a FlumeCode request.`,
752
+ `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)}.`,
753
+ task,
754
+ orient,
755
+ widgets
756
+ ];
757
+ if (ctx.permissionMode !== "plan") {
758
+ lines2.push(
759
+ "",
760
+ "These coding guidelines apply to all code produced in this run:",
761
+ "",
762
+ loadRule("coding-guideline")
763
+ );
764
+ }
765
+ appendRule(lines2, WRITING_INTRO, "technical-writing");
766
+ lines2.push("", `# Request: ${ctx.request?.title ?? ""}`);
767
+ if (ctx.request?.body) {
768
+ lines2.push("", ctx.request.body);
686
769
  }
770
+ appendThread(lines2, ctx);
771
+ lines2.push(
772
+ "",
773
+ 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."
774
+ );
775
+ return lines2.join("\n");
687
776
  }
688
-
689
- // src/executor.ts
690
- import { fileURLToPath as fileURLToPath2 } from "node:url";
691
- import { query } from "@anthropic-ai/claude-agent-sdk";
692
-
693
- // src/widgets.ts
694
- import { randomUUID } from "node:crypto";
695
- import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
696
- import { z } from "zod";
697
- var SERVER_NAME = "flume_widgets";
698
- var SINGLE_SELECT = "single_select";
699
- var MULTI_SELECT = "multi_select";
700
- var WIDGET_TOOL_NAMES = [
701
- `mcp__${SERVER_NAME}__${SINGLE_SELECT}`,
702
- `mcp__${SERVER_NAME}__${MULTI_SELECT}`
703
- ];
704
- var optionsSchema = z.array(z.string().min(1)).min(2).max(8).describe("2\u20138 short, distinct choices for the user to pick from.");
705
- 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.";
706
- function createWidgetTooling() {
707
- const collected = [];
708
- const singleSelect = tool(
709
- SINGLE_SELECT,
710
- "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,
711
- {
712
- question: z.string().min(1).describe("The question to ask the user."),
713
- options: optionsSchema
714
- },
715
- async (args) => {
716
- collected.push({
717
- id: randomUUID(),
718
- type: "single_select",
719
- question: args.question,
720
- options: args.options.map((label) => ({ id: randomUUID(), label })),
721
- selectedOptionId: null,
722
- customAnswer: null
723
- });
724
- return widgetPosted("single-select");
725
- }
777
+ function buildRevisePrompt(ctx) {
778
+ 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.`;
779
+ 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.`;
780
+ 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.`;
781
+ const lines2 = [
782
+ `You are "${ctx.agentName}", an autonomous coding agent fine-tuning an implemented FlumeCode plan in an ongoing thread with the user.`,
783
+ `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.`,
784
+ task,
785
+ orient,
786
+ widgets,
787
+ "",
788
+ "These coding guidelines apply to all code produced in this run:",
789
+ "",
790
+ loadRule("coding-guideline"),
791
+ "",
792
+ WRITING_INTRO,
793
+ "",
794
+ loadRule("technical-writing"),
795
+ "",
796
+ `# Plan: ${ctx.request?.title ?? ""}`
797
+ ];
798
+ if (ctx.request?.body) {
799
+ lines2.push("", ctx.request.body);
800
+ }
801
+ appendThread(lines2, ctx);
802
+ lines2.push(
803
+ "",
804
+ "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."
726
805
  );
727
- const multiSelect = tool(
728
- MULTI_SELECT,
729
- "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,
730
- {
731
- question: z.string().min(1).describe("The question to ask the user."),
732
- options: optionsSchema
733
- },
734
- async (args) => {
735
- collected.push({
736
- id: randomUUID(),
737
- type: "multi_select",
738
- question: args.question,
739
- options: args.options.map((label) => ({ id: randomUUID(), label })),
740
- selectedOptionIds: null,
741
- customAnswer: null
742
- });
743
- return widgetPosted("multi-select");
806
+ return lines2.join("\n");
807
+ }
808
+ function buildResolvePrompt(ctx, related = []) {
809
+ const mergeBranch = ctx.repo.mergeBranch ?? "the merge branch";
810
+ 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.`;
811
+ 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.`;
812
+ const lines2 = [
813
+ `You are "${ctx.agentName}", an autonomous coding agent resolving merge conflicts on an implemented FlumeCode plan.`,
814
+ `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}".`,
815
+ task,
816
+ orient,
817
+ "",
818
+ "These coding guidelines apply to all code produced in this run:",
819
+ "",
820
+ loadRule("coding-guideline"),
821
+ "",
822
+ WRITING_INTRO,
823
+ "",
824
+ loadRule("technical-writing"),
825
+ "",
826
+ `# Plan: ${ctx.request?.title ?? ""}`
827
+ ];
828
+ if (ctx.request?.body) {
829
+ lines2.push("", ctx.request.body);
830
+ }
831
+ appendThread(lines2, ctx);
832
+ if (related.length > 0) {
833
+ lines2.push(
834
+ "",
835
+ "# Related sessions behind the incoming changes",
836
+ "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."
837
+ );
838
+ for (const r of related) {
839
+ lines2.push("", `## PR #${r.prNumber} \u2014 ${r.title}`);
840
+ if (r.plan) lines2.push("", "### Accepted plan", r.plan);
841
+ if (r.report) lines2.push("", "### Final report", r.report);
744
842
  }
843
+ }
844
+ lines2.push(
845
+ "",
846
+ "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)."
745
847
  );
746
- const mcpServer = createSdkMcpServer({
747
- name: SERVER_NAME,
748
- tools: [singleSelect, multiSelect]
749
- });
750
- return { mcpServer, collected };
751
- }
752
- function widgetPosted(kind) {
753
- return {
754
- content: [
755
- {
756
- type: "text",
757
- 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.`
758
- }
759
- ]
760
- };
761
- }
762
-
763
- // src/plan.ts
764
- import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
765
- import { z as z2 } from "zod";
766
-
767
- // src/schema-hints.ts
768
- var INLINE_CODE_HINT = "Wrap code identifiers (function, variable, type, and file names, commands, and flags) in inline backticks, e.g. `getCodingSessionsForRequest`.";
769
-
770
- // src/plan.ts
771
- var SERVER_NAME2 = "flume_plan";
772
- var SUBMIT_PLAN = "submit_plan";
773
- var PLAN_TOOL_NAME = `mcp__${SERVER_NAME2}__${SUBMIT_PLAN}`;
774
- var PLAN_MARKER = "<!-- flumecode:end-of-plan -->";
775
- var pseudoCodeEntrySchema = z2.object({
776
- file: z2.string().min(1),
777
- pseudoCode: z2.string().min(1)
778
- });
779
- var stepSchema = z2.object({
780
- title: z2.string().min(1).describe("A concise imperative title for this step."),
781
- description: z2.array(z2.string().min(1)).min(1).describe(
782
- "Bullet points that explain this step's change so a reviewer can judge whether the design is correct. Each array item is one short, self-contained bullet \u2014 not a single paragraph, and not a restatement of the pseudo code. " + INLINE_CODE_HINT
783
- ),
784
- pseudoCode: z2.array(pseudoCodeEntrySchema).optional().describe(
785
- "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."
786
- )
787
- });
788
- var planInputSchema = {
789
- title: z2.string().min(1).max(120).describe(
790
- "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."
791
- ),
792
- scope: z2.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
793
- goal: z2.string().min(1).describe("One or two sentences stating the outcome. " + INLINE_CODE_HINT),
794
- assumptions: z2.array(z2.string()).describe("Anything decided during planning, including unanswered defaults."),
795
- requirements: z2.array(z2.string().min(1)).min(1).describe(
796
- "Required, human-readable statements of what this change must accomplish and why, in plain language a non-technical reader can follow. Distinct from acceptanceCriteria: requirements explain intent/rationale; acceptance criteria are the machine-checkable proof. At least 1 required. " + INLINE_CODE_HINT
797
- ),
798
- steps: z2.array(stepSchema).min(1).describe("Ordered list of changes. Each step says what and why, with file references."),
799
- acceptanceCriteria: z2.array(z2.string().min(1)).min(2).describe(
800
- "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. " + INLINE_CODE_HINT
801
- ),
802
- risks: z2.array(z2.string()).describe("Anything that could change the approach."),
803
- outOfScope: z2.array(z2.string()).describe("What is deliberately not being done.")
804
- };
805
- var planSchema = z2.object(planInputSchema);
806
- function renderPlan(plan) {
807
- const lines2 = [];
808
- lines2.push(`# ${plan.title}`);
809
- lines2.push("");
810
- lines2.push(`**Scope** \u2014 \`${plan.scope}\``);
811
- lines2.push("");
812
- lines2.push(`**Goal** \u2014 ${plan.goal}`);
813
- if (plan.assumptions.length > 0) {
814
- lines2.push("");
815
- lines2.push("**Assumptions**");
816
- for (const assumption of plan.assumptions) {
817
- lines2.push(`- ${assumption}`);
818
- }
819
- }
820
- lines2.push("");
821
- lines2.push("## Requirements");
822
- for (const requirement of plan.requirements) {
823
- lines2.push(`- ${requirement}`);
824
- }
825
- lines2.push("");
826
- lines2.push("## Steps");
827
- for (const [i, step] of plan.steps.entries()) {
828
- lines2.push("");
829
- lines2.push(`### ${i + 1}. ${step.title}`);
830
- lines2.push("");
831
- for (const bullet of step.description) {
832
- lines2.push(`- ${bullet}`);
833
- }
834
- if (step.pseudoCode && step.pseudoCode.length > 0) {
835
- for (const entry of step.pseudoCode) {
836
- lines2.push("");
837
- lines2.push(`\`${entry.file}\``);
838
- lines2.push("");
839
- lines2.push("```");
840
- lines2.push(entry.pseudoCode);
841
- lines2.push("```");
842
- }
843
- }
844
- }
845
- lines2.push("");
846
- lines2.push("## Acceptance criteria");
847
- for (const criterion of plan.acceptanceCriteria) {
848
- lines2.push(`- [ ] ${criterion}`);
849
- }
850
- if (plan.risks.length > 0) {
851
- lines2.push("");
852
- lines2.push("**Risks / open questions**");
853
- for (const risk of plan.risks) {
854
- lines2.push(`- ${risk}`);
855
- }
856
- }
857
- if (plan.outOfScope.length > 0) {
858
- lines2.push("");
859
- lines2.push("**Out of scope**");
860
- for (const item of plan.outOfScope) {
861
- lines2.push(`- ${item}`);
862
- }
863
- }
864
- lines2.push("");
865
- lines2.push(PLAN_MARKER);
866
- return lines2.join("\n");
867
- }
868
- var submitPlanInputSchema = {
869
- plans: z2.array(z2.object(planInputSchema)).min(1).refine(
870
- (arr) => {
871
- const titles = arr.map((p) => p.title.trim()).filter((t) => t.length > 0);
872
- return new Set(titles).size === titles.length;
873
- },
874
- { message: "Each plan must have a distinct non-empty title" }
875
- )
876
- };
877
- var submitPlanSchema = z2.object(submitPlanInputSchema);
878
- function createPlanTooling() {
879
- let renderedPlans = null;
880
- const submitPlan = tool2(
881
- SUBMIT_PLAN,
882
- "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. requirements is required in each plan: at least 1 plain-language statement of what the change must accomplish and why (human-readable intent), separate from the machine-checkable acceptanceCriteria. ",
883
- submitPlanInputSchema,
884
- async (args) => {
885
- const parsed = submitPlanSchema.parse(args);
886
- renderedPlans = parsed.plans.map(renderPlan);
887
- return {
888
- content: [
889
- {
890
- type: "text",
891
- text: "Plan(s) submitted. The runner will render and post them as your comment(s). End your turn now."
892
- }
893
- ]
894
- };
895
- }
896
- );
897
- const mcpServer = createSdkMcpServer2({
898
- name: SERVER_NAME2,
899
- tools: [submitPlan]
900
- });
901
- return { mcpServer, getPlans: () => renderedPlans };
902
- }
903
- function countPlanAcceptanceCriteria(planBody) {
904
- if (!planBody) return 0;
905
- const lines2 = planBody.split("\n");
906
- const start2 = lines2.findIndex((l) => l.trim() === "## Acceptance criteria");
907
- if (start2 === -1) return 0;
908
- let count = 0;
909
- for (let i = start2 + 1; i < lines2.length; i++) {
910
- const line = lines2[i] ?? "";
911
- if (line.startsWith("## ")) break;
912
- if (line.startsWith("- [ ] ")) count++;
913
- }
914
- return count;
915
- }
916
-
917
- // src/report.ts
918
- import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
919
- import { z as z3 } from "zod";
920
- var SERVER_NAME3 = "flume_report";
921
- var SUBMIT_REPORT = "submit_report";
922
- var REPORT_TOOL_NAME = `mcp__${SERVER_NAME3}__${SUBMIT_REPORT}`;
923
- var STATUS_ICON = {
924
- met: "\u2705",
925
- not_met: "\u274C",
926
- unclear: "\u26A0\uFE0F"
927
- };
928
- var evidenceSchema = z3.object({
929
- file: z3.string().min(1).describe("Repo-relative path the hunk comes from."),
930
- hunk: z3.string().min(1).describe(
931
- "A unified-diff hunk proving the criterion \u2014 the lines that matter, not the whole file. MUST keep the `@@ -a,b +c,d @@` hunk header line(s) exactly as they appear in `git --no-pager diff`; the report renders file line numbers from them. Rendered verbatim as a ```diff block."
932
- ),
933
- note: z3.string().optional().describe("Optional one-line explanation of why this hunk satisfies the criterion.")
934
- });
935
- var acVerdictSchema = z3.object({
936
- criterion: z3.string().min(1).describe("The acceptance-criterion text, verbatim from the plan."),
937
- status: z3.enum(["met", "not_met", "unclear"]).describe("Verdict for this criterion, verified against the actual diff."),
938
- rationale: z3.string().min(1).describe("One or two sentences on why the verdict holds. " + INLINE_CODE_HINT),
939
- evidence: z3.array(evidenceSchema).describe(
940
- "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."
941
- )
942
- });
943
- var reportInputSchema = {
944
- summary: z3.string().min(1).describe("One or two sentences on what was implemented. " + INLINE_CODE_HINT),
945
- filesChanged: z3.string().min(1).describe(
946
- "Markdown: the list of files changed (from the diff). Rendered under '## Files changed'."
947
- ),
948
- codeQuality: z3.string().min(1).describe(
949
- "Markdown: the code-quality review outcome and anything left as nice-to-have. Rendered under '## Code quality'. " + INLINE_CODE_HINT
950
- ),
951
- caveats: z3.string().min(1).describe(
952
- "Markdown: anything deferred, unmet, or worth a human's eyes, incl. diff hunks that map to no plan AC. Write 'None.' if nothing. Rendered under '## Caveats / follow-ups'. " + INLINE_CODE_HINT
953
- ),
954
- acceptanceCriteria: z3.array(acVerdictSchema).describe(
955
- "One entry per acceptance criterion from the plan, in plan order, each with a verdict and the diff evidence behind it. May be empty for resolve runs (no plan to verify)."
956
- ),
957
- conflictResolution: z3.string().optional().describe(
958
- "Markdown: present ONLY when a merge conflict was actually resolved. Explain, per conflicted file, how ours/theirs were integrated. Rendered under '## Conflict resolution'. Omit entirely when no conflict occurred."
959
- )
960
- };
961
- var reportSchema = z3.object(reportInputSchema);
962
- function renderReport(report) {
963
- const lines2 = [];
964
- lines2.push(report.summary.trim());
965
- lines2.push("", "## Files changed", "", report.filesChanged.trim());
966
- if (report.acceptanceCriteria.length > 0) {
967
- lines2.push("", "## Acceptance criteria");
968
- for (const ac of report.acceptanceCriteria) {
969
- lines2.push("");
970
- lines2.push(`### ${STATUS_ICON[ac.status]} ${ac.criterion}`);
971
- lines2.push("");
972
- lines2.push(ac.rationale.trim());
973
- for (const ev of ac.evidence) {
974
- lines2.push("");
975
- lines2.push(ev.note ? `\`${ev.file}\` \u2014 ${ev.note}` : `\`${ev.file}\``);
976
- lines2.push("");
977
- lines2.push("```diff");
978
- lines2.push(ev.hunk.replace(/\n+$/, ""));
979
- lines2.push("```");
980
- }
981
- }
982
- }
983
- if (report.conflictResolution?.trim()) {
984
- lines2.push("", "## Conflict resolution", "", report.conflictResolution.trim());
985
- }
986
- lines2.push("", "## Code quality", "", report.codeQuality.trim());
987
- lines2.push("", "## Caveats / follow-ups", "", report.caveats.trim());
988
848
  return lines2.join("\n");
989
849
  }
990
- function createReportTooling() {
991
- let submittedReport = null;
992
- const submitReport = tool3(
993
- SUBMIT_REPORT,
994
- "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`, `filesChanged`, `codeQuality`, and `caveats` are the four named markdown sections. Do NOT include a PR link \u2014 the runner appends it.",
995
- reportInputSchema,
996
- async (args) => {
997
- submittedReport = reportSchema.parse(args);
998
- return {
999
- content: [
1000
- {
1001
- type: "text",
1002
- text: "Report submitted. The runner will render and post it. End your turn now."
1003
- }
1004
- ]
1005
- };
1006
- }
1007
- );
1008
- const mcpServer = createSdkMcpServer3({
1009
- name: SERVER_NAME3,
1010
- tools: [submitReport]
1011
- });
1012
- return { mcpServer, getReport: () => submittedReport };
1013
- }
1014
-
1015
- // src/executor.ts
1016
- var FLUME_PLUGIN_DIR = fileURLToPath2(new URL("../skills-plugin", import.meta.url));
1017
- function emptyUsage() {
1018
- return {
1019
- inputTokens: 0,
1020
- outputTokens: 0,
1021
- cacheCreationTokens: 0,
1022
- cacheReadTokens: 0,
1023
- costUsd: 0
1024
- };
1025
- }
1026
- var usageAcc = emptyUsage();
1027
- function resetUsage() {
1028
- usageAcc = emptyUsage();
1029
- }
1030
- function getUsage() {
1031
- const totalTokens = usageAcc.inputTokens + usageAcc.outputTokens + usageAcc.cacheCreationTokens + usageAcc.cacheReadTokens;
1032
- return { ...usageAcc, totalTokens };
1033
- }
1034
- function stringifyResult(content) {
1035
- if (typeof content === "string") return content;
1036
- if (Array.isArray(content)) {
1037
- return content.map(
1038
- (c) => typeof c === "object" && c !== null && "text" in c ? String(c.text) : JSON.stringify(c)
1039
- ).join("\n");
1040
- }
1041
- return JSON.stringify(content);
1042
- }
1043
- async function runClaudeCode(opts) {
1044
- let finalText = "";
1045
- const { mcpServer, collected } = createWidgetTooling();
1046
- const { mcpServer: planServer, getPlans } = createPlanTooling();
1047
- const { mcpServer: reportServer, getReport } = createReportTooling();
1048
- for await (const message of query({
1049
- prompt: opts.prompt,
1050
- options: {
1051
- cwd: opts.cwd,
1052
- permissionMode: opts.permissionMode,
1053
- allowDangerouslySkipPermissions: opts.permissionMode === "bypassPermissions",
1054
- ...opts.model ? { model: opts.model } : {},
1055
- ...opts.abortController ? { abortController: opts.abortController } : {},
1056
- maxTurns: opts.maxTurns ?? 40,
1057
- // Hermetic: ignore ALL config from the checked-out repo (CLAUDE.md,
1058
- // .claude/settings.json, .claude/skills, …) and the runner owner's
1059
- // ~/.claude. Only our plugin's skills are exposed to the agent. Auto-memory
1060
- // is disabled separately via CLAUDE_CODE_DISABLE_AUTO_MEMORY (see cli.ts).
1061
- settingSources: [],
1062
- plugins: [{ type: "local", path: FLUME_PLUGIN_DIR }],
1063
- // Pre-approve the widget, plan, and Task tools so they run without a
1064
- // permission prompt in headless mode (allow-listing pre-approves them; it
1065
- // does NOT restrict anything else). Task lets the implement-plan
1066
- // orchestrator spawn its subagents; without pre-approval the spawn could
1067
- // stall waiting for an approval no one can give.
1068
- mcpServers: { flume_widgets: mcpServer, flume_plan: planServer, flume_report: reportServer },
1069
- allowedTools: [...WIDGET_TOOL_NAMES, PLAN_TOOL_NAME, REPORT_TOOL_NAME, "Task"]
1070
- }
1071
- })) {
1072
- if (message.type === "assistant") {
1073
- const content = message.message?.content;
1074
- if (Array.isArray(content)) {
1075
- for (const block of content) {
1076
- if (block && block.type === "text" && typeof block.text === "string") {
1077
- process.stdout.write(block.text);
1078
- logEvent("agent", block.text);
1079
- } else if (block && block.type === "tool_use") {
1080
- logEvent("tool_use", `${block.name} ${JSON.stringify(block.input)}`);
1081
- }
1082
- }
1083
- }
1084
- } else if (message.type === "user") {
1085
- const content = message.message?.content;
1086
- if (Array.isArray(content)) {
1087
- for (const block of content) {
1088
- if (block && block.type === "tool_result") {
1089
- logEvent("tool_result", stringifyResult(block.content));
1090
- }
1091
- }
1092
- }
1093
- } else if (message.type === "result") {
1094
- finalText = message.result ?? "";
1095
- logEvent("result", finalText);
1096
- const resultMsg = message;
1097
- if (resultMsg.usage) {
1098
- usageAcc.inputTokens += resultMsg.usage.input_tokens ?? 0;
1099
- usageAcc.outputTokens += resultMsg.usage.output_tokens ?? 0;
1100
- usageAcc.cacheCreationTokens += resultMsg.usage.cache_creation_input_tokens ?? 0;
1101
- usageAcc.cacheReadTokens += resultMsg.usage.cache_read_input_tokens ?? 0;
1102
- }
1103
- usageAcc.costUsd += resultMsg.total_cost_usd ?? 0;
1104
- } else if (message.type === "system") {
1105
- logEvent("system", JSON.stringify(message));
1106
- }
1107
- }
1108
- process.stdout.write("\n");
1109
- if (opts.abortController?.signal.aborted) {
1110
- throw new Error("Run canceled by user");
1111
- }
1112
- return { text: finalText, widgets: collected, plans: getPlans(), report: getReport() };
1113
- }
1114
-
1115
- // src/health.ts
1116
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1117
- var PROBE_TIMEOUT_MS = 6e4;
1118
- async function checkClaudeCode() {
1119
- try {
1120
- let sawResult = false;
1121
- const run = (async () => {
1122
- for await (const message of query2({
1123
- prompt: "Reply with the single word: ok",
1124
- options: { permissionMode: "bypassPermissions", maxTurns: 1 }
1125
- })) {
1126
- if (message.type === "result") {
1127
- sawResult = true;
1128
- const isError = message.is_error === true;
1129
- const subtype = message.subtype;
1130
- if (isError)
1131
- throw new Error(
1132
- subtype ? `Claude Code error: ${subtype}` : "Claude Code returned an error"
1133
- );
1134
- }
1135
- }
1136
- })();
1137
- await withTimeout(run, PROBE_TIMEOUT_MS, "Claude Code did not respond in time");
1138
- if (!sawResult) return { ready: false, error: "Claude Code produced no result" };
1139
- return { ready: true };
1140
- } catch (err) {
1141
- return { ready: false, error: errorMessage(err) };
1142
- }
1143
- }
1144
- function withTimeout(p, ms, message) {
1145
- return new Promise((resolve, reject) => {
1146
- const timer = setTimeout(() => reject(new Error(message)), ms);
1147
- p.then(
1148
- (v) => {
1149
- clearTimeout(timer);
1150
- resolve(v);
1151
- },
1152
- (e) => {
1153
- clearTimeout(timer);
1154
- reject(e);
1155
- }
1156
- );
1157
- });
1158
- }
1159
- function errorMessage(err) {
1160
- return err instanceof Error ? err.message : String(err);
1161
- }
1162
-
1163
- // src/rules.ts
1164
- import { readFileSync as readFileSync3 } from "node:fs";
1165
- import { join as join5 } from "node:path";
1166
- import { fileURLToPath as fileURLToPath3 } from "node:url";
1167
- var RULES_DIR = fileURLToPath3(new URL("../skills-plugin/rules", import.meta.url));
1168
- function loadRule(name) {
1169
- const raw = readFileSync3(join5(RULES_DIR, `${name}.md`), "utf8");
1170
- return stripFrontMatter(raw).trim();
1171
- }
1172
- function stripFrontMatter(raw) {
1173
- const match = raw.match(/^---\n.*?\n---\n/s);
1174
- return match ? raw.slice(match[0].length) : raw;
1175
- }
1176
-
1177
- // src/prompt.ts
1178
- function appendRule(lines2, intro, ruleName) {
1179
- lines2.push("", intro, "", loadRule(ruleName));
1180
- }
1181
- var WRITING_INTRO = "These technical-writing guidelines apply to the plan and report prose you author in this run:";
1182
- function turnHeading(turn, agentName) {
1183
- if (turn.role === "user") return "User";
1184
- if (turn.failed) return `${agentName} (this run ended in an error)`;
1185
- if (turn.kind === "plan") return `${agentName} (proposed a plan)`;
1186
- if (turn.kind === "report") return `${agentName} (implementation report)`;
1187
- return agentName;
1188
- }
1189
- function appendThread(lines2, ctx) {
1190
- if (!ctx.thread || ctx.thread.length === 0) return;
1191
- lines2.push("", "# Conversation so far");
1192
- for (const turn of ctx.thread) {
1193
- lines2.push("", `## ${turnHeading(turn, ctx.agentName)}`, turn.content);
1194
- }
1195
- }
1196
- function buildPrompt(ctx) {
1197
- 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.`;
1198
- 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.`;
1199
- 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.`;
850
+ function buildDocumentPrompt(ctx, changedFiles) {
1200
851
  const lines2 = [
1201
- `You are "${ctx.agentName}", an autonomous coding agent working inside a FlumeCode request.`,
1202
- `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)}.`,
1203
- task,
1204
- orient,
1205
- widgets
852
+ `You are "${ctx.agentName}" maintaining the repository wiki for ${ctx.repo.fullName}.`,
853
+ `An implementation just ran in this working directory to satisfy the request below; its changes are uncommitted in the working tree.`,
854
+ `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.`,
855
+ "",
856
+ `# Request: ${ctx.request?.title ?? ""}`
1206
857
  ];
1207
- if (ctx.permissionMode !== "plan") {
858
+ if (ctx.request?.body) {
859
+ lines2.push("", ctx.request.body);
860
+ }
861
+ appendThread(lines2, ctx);
862
+ if (changedFiles && changedFiles.trim()) {
1208
863
  lines2.push(
1209
864
  "",
1210
- "These coding guidelines apply to all code produced in this run:",
865
+ "Files changed by this implementation (reconcile only the wiki pages these affect \u2014 do not re-survey the whole repo):",
1211
866
  "",
1212
- loadRule("coding-guideline")
867
+ changedFiles.trim()
1213
868
  );
1214
869
  }
1215
- appendRule(lines2, WRITING_INTRO, "technical-writing");
1216
- lines2.push("", `# Request: ${ctx.request?.title ?? ""}`);
1217
- if (ctx.request?.body) {
1218
- lines2.push("", ctx.request.body);
1219
- }
1220
- appendThread(lines2, ctx);
1221
- lines2.push(
1222
- "",
1223
- 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."
1224
- );
870
+ lines2.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
1225
871
  return lines2.join("\n");
1226
- }
1227
- function buildRevisePrompt(ctx) {
1228
- 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.`;
1229
- 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.`;
1230
- 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.`;
1231
- const lines2 = [
1232
- `You are "${ctx.agentName}", an autonomous coding agent fine-tuning an implemented FlumeCode plan in an ongoing thread with the user.`,
1233
- `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.`,
1234
- task,
1235
- orient,
1236
- widgets,
872
+ }
873
+ function buildRepairPrompt(ctx, hookLog) {
874
+ const lines2 = [
875
+ `You are "${ctx.agentName}", fixing a failed pre-commit check in the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
876
+ `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.`,
1237
877
  "",
1238
878
  "These coding guidelines apply to all code produced in this run:",
1239
879
  "",
1240
880
  loadRule("coding-guideline"),
1241
881
  "",
1242
- WRITING_INTRO,
882
+ "# Pre-commit hook output",
1243
883
  "",
1244
- loadRule("technical-writing"),
884
+ "```",
885
+ hookLog,
886
+ "```",
1245
887
  "",
1246
- `# Plan: ${ctx.request?.title ?? ""}`
888
+ "When done, reply with a one-line summary of what you fixed."
1247
889
  ];
1248
- if (ctx.request?.body) {
1249
- lines2.push("", ctx.request.body);
1250
- }
1251
- appendThread(lines2, ctx);
1252
- lines2.push(
1253
- "",
1254
- "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."
1255
- );
1256
890
  return lines2.join("\n");
1257
891
  }
1258
- function buildResolvePrompt(ctx, related = []) {
1259
- const mergeBranch = ctx.repo.mergeBranch ?? "the merge branch";
1260
- 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.`;
1261
- 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.`;
892
+ function buildReleasePrompt(ctx, baseChecks) {
893
+ 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.`;
894
+ 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.`;
895
+ 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.`;
1262
896
  const lines2 = [
1263
- `You are "${ctx.agentName}", an autonomous coding agent resolving merge conflicts on an implemented FlumeCode plan.`,
1264
- `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}".`,
897
+ `You are "${ctx.agentName}", an autonomous coding agent driving a FlumeCode release.`,
898
+ `The repository ${ctx.repo.fullName} is checked out in your current working directory on the release bump branch "${ctx.repo.checkoutBranch}".`,
1265
899
  task,
1266
900
  orient,
901
+ widgets,
1267
902
  "",
1268
903
  "These coding guidelines apply to all code produced in this run:",
1269
904
  "",
1270
905
  loadRule("coding-guideline"),
1271
906
  "",
1272
- WRITING_INTRO,
1273
- "",
1274
- loadRule("technical-writing"),
1275
- "",
1276
- `# Plan: ${ctx.request?.title ?? ""}`
907
+ `# Release: ${ctx.request?.title ?? ""}`
1277
908
  ];
1278
909
  if (ctx.request?.body) {
1279
910
  lines2.push("", ctx.request.body);
1280
911
  }
1281
- appendThread(lines2, ctx);
1282
- if (related.length > 0) {
912
+ if (baseChecks && !baseChecks.ok) {
1283
913
  lines2.push(
1284
914
  "",
1285
- "# Related sessions behind the incoming changes",
1286
- "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."
915
+ "# Pre-release check status",
916
+ "",
917
+ "\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:",
918
+ "",
919
+ "- **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.",
920
+ "- **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.",
921
+ "",
922
+ "Failing check output:",
923
+ "",
924
+ "```",
925
+ baseChecks.log,
926
+ "```"
1287
927
  );
1288
- for (const r of related) {
1289
- lines2.push("", `## PR #${r.prNumber} \u2014 ${r.title}`);
1290
- if (r.plan) lines2.push("", "### Accepted plan", r.plan);
1291
- if (r.report) lines2.push("", "### Final report", r.report);
928
+ }
929
+ appendThread(lines2, ctx);
930
+ lines2.push(
931
+ "",
932
+ "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."
933
+ );
934
+ return lines2.join("\n");
935
+ }
936
+ function buildInitPrompt(ctx) {
937
+ return [
938
+ `You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
939
+ `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.`,
940
+ "",
941
+ "When done, reply with a one- or two-line summary of the wiki you created."
942
+ ].join("\n");
943
+ }
944
+
945
+ // src/types.ts
946
+ function jobTitle(ctx) {
947
+ return ctx.kind === "init" ? "Initialize FlumeCode wiki" : ctx.request?.title ?? "request";
948
+ }
949
+
950
+ // src/workspace.ts
951
+ import { execFile } from "node:child_process";
952
+ import { existsSync as existsSync2 } from "node:fs";
953
+ import { mkdtemp, readdir, rm } from "node:fs/promises";
954
+ import { tmpdir } from "node:os";
955
+ import { join as join3 } from "node:path";
956
+ import { promisify } from "node:util";
957
+ var exec = promisify(execFile);
958
+ var WORKSPACE_PREFIX = "flume-runner-";
959
+ var MAX_BUFFER = 1 << 24;
960
+ async function git(args) {
961
+ logEvent("git", `git ${args.join(" ")}`);
962
+ try {
963
+ const result = await exec("git", args, { maxBuffer: MAX_BUFFER });
964
+ if (result.stdout.trim()) logEvent("git:out", result.stdout.trim());
965
+ if (result.stderr.trim()) logEvent("git:err", result.stderr.trim());
966
+ return result;
967
+ } catch (err) {
968
+ logEvent("git:err", String(err.stderr ?? err));
969
+ throw err;
970
+ }
971
+ }
972
+ async function ensureGitIdentity(dir, identity) {
973
+ await git(["-C", dir, "config", "user.email", identity.email]);
974
+ await git(["-C", dir, "config", "user.name", identity.name]);
975
+ }
976
+ function cloneUrl(ctx) {
977
+ const { owner, name, cloneToken } = ctx.repo;
978
+ return `https://x-access-token:${cloneToken}@github.com/${owner}/${name}.git`;
979
+ }
980
+ function detectPackageManager(dir) {
981
+ if (!existsSync2(join3(dir, "package.json"))) return null;
982
+ if (existsSync2(join3(dir, "pnpm-lock.yaml"))) return "pnpm";
983
+ if (existsSync2(join3(dir, "yarn.lock"))) return "yarn";
984
+ if (existsSync2(join3(dir, "package-lock.json"))) return "npm";
985
+ if (existsSync2(join3(dir, "bun.lockb"))) return "bun";
986
+ return "npm";
987
+ }
988
+ async function installDependencies(dir) {
989
+ const manager = detectPackageManager(dir);
990
+ if (manager === null) return { status: "skipped" };
991
+ const env = { ...process.env, CI: "1", ADBLOCK: "1", DISABLE_OPENCOLLECTIVE: "1" };
992
+ logEvent("install", `${manager} install`);
993
+ try {
994
+ const result = await exec(manager, ["install"], {
995
+ cwd: dir,
996
+ maxBuffer: MAX_BUFFER,
997
+ env,
998
+ timeout: 5 * 6e4
999
+ });
1000
+ if (result.stdout.trim()) logEvent("install:out", result.stdout.trim());
1001
+ if (result.stderr.trim()) logEvent("install:err", result.stderr.trim());
1002
+ return { status: "installed", manager };
1003
+ } catch (err) {
1004
+ const e = err;
1005
+ const detail = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean).join("\n");
1006
+ logEvent("install:err", detail || (err instanceof Error ? err.message : String(err)));
1007
+ return { status: "failed", manager, error: err instanceof Error ? err.message : String(err) };
1008
+ }
1009
+ }
1010
+ async function makeWorkspace() {
1011
+ return mkdtemp(join3(tmpdir(), WORKSPACE_PREFIX));
1012
+ }
1013
+ var MAX_WORKSPACES = 8;
1014
+ var workspaceRegistry = /* @__PURE__ */ new Map();
1015
+ async function acquireWorkspace(key) {
1016
+ const existing = workspaceRegistry.get(key);
1017
+ if (existing !== void 0 && existsSync2(existing)) {
1018
+ workspaceRegistry.delete(key);
1019
+ workspaceRegistry.set(key, existing);
1020
+ return { dir: existing, reused: true };
1021
+ }
1022
+ const dir = await makeWorkspace();
1023
+ workspaceRegistry.set(key, dir);
1024
+ if (workspaceRegistry.size > MAX_WORKSPACES) {
1025
+ const oldest = workspaceRegistry.keys().next().value;
1026
+ const oldDir = workspaceRegistry.get(oldest);
1027
+ workspaceRegistry.delete(oldest);
1028
+ rm(oldDir, { recursive: true, force: true }).catch(() => {
1029
+ });
1030
+ }
1031
+ return { dir, reused: false };
1032
+ }
1033
+ async function discardWorkspace(key) {
1034
+ const dir = workspaceRegistry.get(key);
1035
+ workspaceRegistry.delete(key);
1036
+ if (dir !== void 0) {
1037
+ await cleanup(dir).catch(() => {
1038
+ });
1039
+ }
1040
+ }
1041
+ async function resetWorkspace(dir) {
1042
+ await git(["-C", dir, "reset", "--hard", "HEAD"]).catch(() => {
1043
+ });
1044
+ await git(["-C", dir, "clean", "-fd"]).catch(() => {
1045
+ });
1046
+ }
1047
+ async function prepareAtSha(ctx, dir, reused) {
1048
+ const identity = { name: ctx.agentName, email: ctx.agentEmail };
1049
+ if (!reused) {
1050
+ await cloneAtSha(ctx, dir);
1051
+ await ensureGitIdentity(dir, identity);
1052
+ return;
1053
+ }
1054
+ await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
1055
+ await ensureGitIdentity(dir, identity);
1056
+ }
1057
+ async function prepareResumingBranch(ctx, dir, reused) {
1058
+ const identity = { name: ctx.agentName, email: ctx.agentEmail };
1059
+ if (!reused) {
1060
+ const result = await cloneResumingBranch(ctx, dir);
1061
+ await ensureGitIdentity(dir, identity);
1062
+ return result;
1063
+ }
1064
+ await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
1065
+ await ensureGitIdentity(dir, identity);
1066
+ return { resumed: true };
1067
+ }
1068
+ async function sweepWorkspaces() {
1069
+ const base = tmpdir();
1070
+ let entries;
1071
+ try {
1072
+ entries = await readdir(base);
1073
+ } catch {
1074
+ return 0;
1075
+ }
1076
+ let removed = 0;
1077
+ for (const entry of entries) {
1078
+ if (!entry.startsWith(WORKSPACE_PREFIX)) continue;
1079
+ try {
1080
+ await rm(join3(base, entry), { recursive: true, force: true });
1081
+ removed++;
1082
+ } catch {
1083
+ }
1084
+ }
1085
+ return removed;
1086
+ }
1087
+ async function cloneAtSha(ctx, dir) {
1088
+ await git(["clone", "--quiet", cloneUrl(ctx), dir]);
1089
+ await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
1090
+ }
1091
+ async function cloneResumingBranch(ctx, dir) {
1092
+ await git(["clone", "--quiet", cloneUrl(ctx), dir]);
1093
+ try {
1094
+ await git(["-C", dir, "fetch", "--quiet", "origin", ctx.repo.checkoutBranch]);
1095
+ await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, "FETCH_HEAD"]);
1096
+ return { resumed: true };
1097
+ } catch {
1098
+ await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
1099
+ return { resumed: false };
1100
+ }
1101
+ }
1102
+ async function hasChanges(dir) {
1103
+ await git(["-C", dir, "add", "-A"]);
1104
+ const { stdout: stdout2 } = await git(["-C", dir, "status", "--porcelain"]);
1105
+ return stdout2.trim().length > 0;
1106
+ }
1107
+ async function gitDiffStat(dir) {
1108
+ const { stdout: stdout2 } = await git(["-C", dir, "--no-pager", "diff", "--stat"]);
1109
+ return stdout2;
1110
+ }
1111
+ var PreCommitError = class extends Error {
1112
+ constructor(log) {
1113
+ super("pre-commit checks failed");
1114
+ this.log = log;
1115
+ this.name = "PreCommitError";
1116
+ }
1117
+ };
1118
+ function commitFailureLog(err) {
1119
+ const e = err;
1120
+ const parts = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter((s) => s.length > 0);
1121
+ return parts.length > 0 ? parts.join("\n") : e.message ?? String(err);
1122
+ }
1123
+ function isUnsupportedGitSubcommand(err) {
1124
+ const e = err;
1125
+ const text = `${typeof e.stderr === "string" ? e.stderr : ""}
1126
+ ${e.message ?? ""}`;
1127
+ return /is not a git command|unknown subcommand|usage: git hook/i.test(text);
1128
+ }
1129
+ async function runRepoChecks(dir) {
1130
+ try {
1131
+ await git(["-C", dir, "hook", "run", "pre-commit"]);
1132
+ logEvent("checks", "pre-commit hook passed");
1133
+ return { ok: true, log: "", skipped: false };
1134
+ } catch (err) {
1135
+ if (isUnsupportedGitSubcommand(err)) {
1136
+ logEvent("checks", "pre-commit hook skipped (git too old)");
1137
+ return { ok: true, log: "", skipped: true };
1292
1138
  }
1139
+ const log = commitFailureLog(err);
1140
+ logEvent("checks:err", log);
1141
+ return { ok: false, log, skipped: false };
1293
1142
  }
1294
- lines2.push(
1295
- "",
1296
- "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)."
1297
- );
1298
- return lines2.join("\n");
1299
1143
  }
1300
- function buildDocumentPrompt(ctx, changedFiles) {
1301
- const lines2 = [
1302
- `You are "${ctx.agentName}" maintaining the repository wiki for ${ctx.repo.fullName}.`,
1303
- `An implementation just ran in this working directory to satisfy the request below; its changes are uncommitted in the working tree.`,
1304
- `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.`,
1305
- "",
1306
- `# Request: ${ctx.request?.title ?? ""}`
1307
- ];
1308
- if (ctx.request?.body) {
1309
- lines2.push("", ctx.request.body);
1144
+ async function commitChanges(ctx, dir) {
1145
+ if (!await hasChanges(dir)) return false;
1146
+ try {
1147
+ await git(["-C", dir, "commit", "--quiet", "-m", `FlumeCode: ${jobTitle(ctx)}`]);
1148
+ } catch (err) {
1149
+ throw new PreCommitError(commitFailureLog(err));
1310
1150
  }
1311
- appendThread(lines2, ctx);
1312
- if (changedFiles && changedFiles.trim()) {
1313
- lines2.push(
1314
- "",
1315
- "Files changed by this implementation (reconcile only the wiki pages these affect \u2014 do not re-survey the whole repo):",
1316
- "",
1317
- changedFiles.trim()
1151
+ return true;
1152
+ }
1153
+ async function pushBranch(ctx, dir) {
1154
+ await git(["-C", dir, "push", "--quiet", "-u", "origin", ctx.repo.checkoutBranch]);
1155
+ }
1156
+ var RebaseConflictError = class extends Error {
1157
+ constructor(mergeBranch, files) {
1158
+ const list = files.length ? `: ${files.join(", ")}` : "";
1159
+ super(`Rebase onto ${mergeBranch} hit conflicts in ${files.length} file(s)${list}`);
1160
+ this.mergeBranch = mergeBranch;
1161
+ this.files = files;
1162
+ this.name = "RebaseConflictError";
1163
+ }
1164
+ };
1165
+ async function rebaseOntoMergeBranch(ctx, dir) {
1166
+ const { mergeBranch } = ctx.repo;
1167
+ if (!mergeBranch) return;
1168
+ await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
1169
+ try {
1170
+ await git(["-C", dir, "rebase", "--empty=drop", "FETCH_HEAD"]);
1171
+ } catch (err) {
1172
+ const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
1173
+ () => ({ stdout: "" })
1318
1174
  );
1175
+ const files = conflicted.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
1176
+ await git(["-C", dir, "rebase", "--abort"]).catch(() => {
1177
+ });
1178
+ if (files.length === 0) throw err;
1179
+ throw new RebaseConflictError(mergeBranch, files);
1319
1180
  }
1320
- lines2.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
1321
- return lines2.join("\n");
1322
1181
  }
1323
- function buildRepairPrompt(ctx, hookLog) {
1324
- const lines2 = [
1325
- `You are "${ctx.agentName}", fixing a failed pre-commit check in the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
1326
- `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.`,
1327
- "",
1328
- "These coding guidelines apply to all code produced in this run:",
1329
- "",
1330
- loadRule("coding-guideline"),
1331
- "",
1332
- "# Pre-commit hook output",
1333
- "",
1334
- "```",
1335
- hookLog,
1336
- "```",
1337
- "",
1338
- "When done, reply with a one-line summary of what you fixed."
1339
- ];
1340
- return lines2.join("\n");
1182
+ async function mergeInMergeBranch(ctx, dir) {
1183
+ const { mergeBranch } = ctx.repo;
1184
+ if (!mergeBranch) return { conflicted: false };
1185
+ await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
1186
+ try {
1187
+ await git(["-C", dir, "merge", "--no-edit", "FETCH_HEAD"]);
1188
+ return { conflicted: false };
1189
+ } catch {
1190
+ return { conflicted: true };
1191
+ }
1341
1192
  }
1342
- function buildReleasePrompt(ctx, baseChecks) {
1343
- 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.`;
1344
- 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.`;
1345
- 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.`;
1346
- const lines2 = [
1347
- `You are "${ctx.agentName}", an autonomous coding agent driving a FlumeCode release.`,
1348
- `The repository ${ctx.repo.fullName} is checked out in your current working directory on the release bump branch "${ctx.repo.checkoutBranch}".`,
1349
- task,
1350
- orient,
1351
- widgets,
1352
- "",
1353
- "These coding guidelines apply to all code produced in this run:",
1354
- "",
1355
- loadRule("coding-guideline"),
1356
- "",
1357
- `# Release: ${ctx.request?.title ?? ""}`
1358
- ];
1359
- if (ctx.request?.body) {
1360
- lines2.push("", ctx.request.body);
1193
+ async function listUnmergedPaths(dir) {
1194
+ const { stdout: stdout2 } = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(() => ({
1195
+ stdout: ""
1196
+ }));
1197
+ return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
1198
+ }
1199
+ async function listConflictMarkerPaths(dir, paths) {
1200
+ if (paths.length === 0) return [];
1201
+ const { stdout: stdout2 } = await git([
1202
+ "-C",
1203
+ dir,
1204
+ "grep",
1205
+ "--no-color",
1206
+ "-lE",
1207
+ "^(<<<<<<<|>>>>>>>|\\|\\|\\|\\|\\|\\|\\|)",
1208
+ "--",
1209
+ ...paths
1210
+ ]).catch(() => ({ stdout: "" }));
1211
+ return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
1212
+ }
1213
+ async function openPullRequest(ctx) {
1214
+ const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
1215
+ if (!mergeBranch) return null;
1216
+ const apiBase = `https://api.github.com/repos/${owner}/${name}`;
1217
+ const headers = {
1218
+ authorization: `Bearer ${cloneToken}`,
1219
+ accept: "application/vnd.github+json",
1220
+ "x-github-api-version": "2022-11-28",
1221
+ "content-type": "application/json"
1222
+ };
1223
+ const title = jobTitle(ctx);
1224
+ 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}".`;
1225
+ const res = await fetch(`${apiBase}/pulls`, {
1226
+ method: "POST",
1227
+ headers,
1228
+ body: JSON.stringify({
1229
+ title: `FlumeCode: ${title}`,
1230
+ head: checkoutBranch,
1231
+ base: mergeBranch,
1232
+ body
1233
+ })
1234
+ });
1235
+ if (res.status === 201) {
1236
+ const data = await res.json();
1237
+ return { number: data.number, url: data.html_url };
1361
1238
  }
1362
- if (baseChecks && !baseChecks.ok) {
1363
- lines2.push(
1364
- "",
1365
- "# Pre-release check status",
1366
- "",
1367
- "\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:",
1368
- "",
1369
- "- **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.",
1370
- "- **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.",
1371
- "",
1372
- "Failing check output:",
1373
- "",
1374
- "```",
1375
- baseChecks.log,
1376
- "```"
1239
+ if (res.status === 422) {
1240
+ const list = await fetch(
1241
+ `${apiBase}/pulls?state=open&head=${owner}:${checkoutBranch}&base=${mergeBranch}`,
1242
+ { headers }
1377
1243
  );
1244
+ if (list.ok) {
1245
+ const open = await list.json();
1246
+ if (open[0]) return { number: open[0].number, url: open[0].html_url };
1247
+ }
1248
+ return null;
1378
1249
  }
1379
- appendThread(lines2, ctx);
1380
- lines2.push(
1381
- "",
1382
- "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."
1383
- );
1384
- return lines2.join("\n");
1250
+ throw new Error(`PR creation failed: ${res.status} ${await res.text()}`);
1385
1251
  }
1386
- function buildInitPrompt(ctx) {
1387
- return [
1388
- `You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
1389
- `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.`,
1390
- "",
1391
- "When done, reply with a one- or two-line summary of the wiki you created."
1392
- ].join("\n");
1252
+ async function cleanup(dir) {
1253
+ await rm(dir, { recursive: true, force: true });
1254
+ }
1255
+ function parsePrFromSubject(subject) {
1256
+ const m = subject.match(/\(#(\d+)\)\s*$/);
1257
+ return m ? Number(m[1]) : null;
1258
+ }
1259
+ async function incomingPrNumbers(ctx, dir, paths) {
1260
+ if (!paths.length) return [];
1261
+ try {
1262
+ const mergeHeadResult = await git(["-C", dir, "rev-parse", "MERGE_HEAD"]);
1263
+ const mergeHead = mergeHeadResult.stdout.trim();
1264
+ const baseResult = await git(["-C", dir, "merge-base", "HEAD", mergeHead]);
1265
+ const base = baseResult.stdout.trim();
1266
+ const logResult = await git([
1267
+ "-C",
1268
+ dir,
1269
+ "log",
1270
+ "--no-merges",
1271
+ `--format=%H%x1f%s`,
1272
+ `${base}..${mergeHead}`,
1273
+ "--",
1274
+ ...paths
1275
+ ]);
1276
+ const nums = /* @__PURE__ */ new Set();
1277
+ const needLookup = [];
1278
+ for (const line of logResult.stdout.split("\n").filter(Boolean)) {
1279
+ const idx = line.indexOf("");
1280
+ const sha = line.slice(0, idx);
1281
+ const subject = line.slice(idx + 1);
1282
+ const n = parsePrFromSubject(subject);
1283
+ if (n !== null) nums.add(n);
1284
+ else needLookup.push(sha);
1285
+ }
1286
+ for (const sha of needLookup) {
1287
+ for (const n of await prNumbersForCommit(ctx, sha)) nums.add(n);
1288
+ }
1289
+ return [...nums];
1290
+ } catch {
1291
+ return [];
1292
+ }
1293
+ }
1294
+ async function prNumbersForCommit(ctx, sha) {
1295
+ const { owner, name, cloneToken } = ctx.repo;
1296
+ try {
1297
+ const res = await fetch(`https://api.github.com/repos/${owner}/${name}/commits/${sha}/pulls`, {
1298
+ headers: {
1299
+ authorization: `Bearer ${cloneToken}`,
1300
+ accept: "application/vnd.github+json",
1301
+ "x-github-api-version": "2022-11-28"
1302
+ }
1303
+ });
1304
+ if (!res.ok) return [];
1305
+ return (await res.json()).map((p) => p.number);
1306
+ } catch {
1307
+ return [];
1308
+ }
1393
1309
  }
1394
1310
 
1395
1311
  // src/run.ts
@@ -1419,7 +1335,7 @@ async function pushAndOpenPr(ctx, dir, config, abort, opts = { rebase: true }) {
1419
1335
  );
1420
1336
  const { report: mergeReport } = await mergeAndResolveConflicts(ctx, dir, config, abort);
1421
1337
  conflictResolution = mergeReport?.conflictResolution;
1422
- await commitWithRepair(ctx, dir, abort, { skipSocket: true });
1338
+ await commitWithRepair(ctx, dir, abort);
1423
1339
  autoMerged = true;
1424
1340
  }
1425
1341
  }
@@ -1453,10 +1369,9 @@ async function mergeAndResolveConflicts(ctx, dir, config, abort) {
1453
1369
  }
1454
1370
  return { resolved: true, text: result.text.trim() || null, report: result.report ?? void 0 };
1455
1371
  }
1456
- async function commitWithRepair(ctx, dir, abort, opts = {}) {
1372
+ async function commitWithRepair(ctx, dir, abort) {
1457
1373
  for (let attempt = 1; ; attempt++) {
1458
1374
  try {
1459
- if (!opts.skipSocket) await runSocket("pre-commit", dir);
1460
1375
  return await commitChanges(ctx, dir);
1461
1376
  } catch (err) {
1462
1377
  if (!(err instanceof PreCommitError) || attempt > MAX_COMMIT_REPAIRS) throw err;
@@ -1581,7 +1496,7 @@ async function processChatJob(ctx, dir, config, abort) {
1581
1496
  console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
1582
1497
  return { text: reply, widgets: result.widgets };
1583
1498
  }
1584
- const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1499
+ const wikiExists = existsSync3(join4(dir, ".flumecode", "wiki"));
1585
1500
  let documented = false;
1586
1501
  if (ctx.permissionMode !== "plan" && wikiExists && await hasChanges(dir)) {
1587
1502
  try {
@@ -1670,7 +1585,7 @@ ${reply}`;
1670
1585
 
1671
1586
  > \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
1672
1587
  }
1673
- const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1588
+ const wikiExists = existsSync3(join4(dir, ".flumecode", "wiki"));
1674
1589
  let documented = false;
1675
1590
  if (wikiExists && await hasChanges(dir)) {
1676
1591
  try {
@@ -1694,9 +1609,7 @@ ${reply}`;
1694
1609
  rebase: !resumed
1695
1610
  });
1696
1611
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1697
- const lintPlugins = getSocketResults();
1698
- const reportWithConflict = report && conflictResolution ? { ...report, conflictResolution } : report;
1699
- const finalReport = reportWithConflict && lintPlugins.length ? { ...reportWithConflict, lint: { plugins: lintPlugins } } : reportWithConflict;
1612
+ const finalReport = report && conflictResolution ? { ...report, conflictResolution } : report;
1700
1613
  return {
1701
1614
  text: reply,
1702
1615
  widgets: [],
@@ -1728,7 +1641,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1728
1641
  console.log(` \u2026revise ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
1729
1642
  return { text: reply, widgets: result.widgets };
1730
1643
  }
1731
- const wikiExists = existsSync4(join6(dir, ".flumecode", "wiki"));
1644
+ const wikiExists = existsSync3(join4(dir, ".flumecode", "wiki"));
1732
1645
  let documented = false;
1733
1646
  if (wikiExists && await hasChanges(dir)) {
1734
1647
  try {
@@ -1754,9 +1667,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
1754
1667
  if (outcome.kind !== "none") {
1755
1668
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
1756
1669
  }
1757
- const lintPlugins = getSocketResults();
1758
- const reportWithConflict = report && conflictResolution ? { ...report, conflictResolution } : report;
1759
- const finalReport = reportWithConflict && lintPlugins.length ? { ...reportWithConflict, lint: { plugins: lintPlugins } } : reportWithConflict;
1670
+ const finalReport = report && conflictResolution ? { ...report, conflictResolution } : report;
1760
1671
  return {
1761
1672
  text: reply,
1762
1673
  widgets: [],
@@ -1782,7 +1693,7 @@ async function processResolveJob(ctx, dir, config, abort) {
1782
1693
  > \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
1783
1694
  }
1784
1695
  if (abort.signal.aborted) throw new Error("Run canceled by user");
1785
- await commitWithRepair(ctx, dir, abort, { skipSocket: true });
1696
+ await commitWithRepair(ctx, dir, abort);
1786
1697
  await pushBranch(ctx, dir);
1787
1698
  const pr = await openPullRequest(ctx);
1788
1699
  const outcome = pr ? { kind: "pr", pr } : { kind: "pushed" };
@@ -1896,7 +1807,6 @@ async function pollLoop(config) {
1896
1807
  scheduleCancelPoll();
1897
1808
  try {
1898
1809
  resetUsage();
1899
- resetSocketResults();
1900
1810
  const { text, widgets, pr, plans, report } = await processJob(ctx, config, abort);
1901
1811
  const usage = getUsage();
1902
1812
  await reportJob(config, ctx.jobId, {