@flumecode/runner 0.18.0 → 0.20.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
|
|
30
|
-
import { join as
|
|
29
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
30
|
+
import { join as join5 } from "node:path";
|
|
31
31
|
|
|
32
32
|
// src/version.ts
|
|
33
33
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -188,6 +188,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
188
188
|
import { randomUUID } from "node:crypto";
|
|
189
189
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
190
190
|
import { z } from "zod";
|
|
191
|
+
|
|
192
|
+
// src/schema-hints.ts
|
|
193
|
+
var INLINE_CODE_HINT = "Wrap code identifiers (function, variable, type, and file names, commands, and flags) in inline backticks, e.g. `getCodingSessionsForRequest`.";
|
|
194
|
+
var WIDGET_LANGUAGE_HINT = "Write this in the same natural language as the incoming thread (the request body and the user's messages). If the thread is in English, keep it in English; do not switch languages. Keep code identifiers, file paths, and quoted code verbatim.";
|
|
195
|
+
|
|
196
|
+
// src/widgets.ts
|
|
191
197
|
var SERVER_NAME = "flume_widgets";
|
|
192
198
|
var SINGLE_SELECT = "single_select";
|
|
193
199
|
var MULTI_SELECT = "multi_select";
|
|
@@ -195,53 +201,61 @@ var WIDGET_TOOL_NAMES = [
|
|
|
195
201
|
`mcp__${SERVER_NAME}__${SINGLE_SELECT}`,
|
|
196
202
|
`mcp__${SERVER_NAME}__${MULTI_SELECT}`
|
|
197
203
|
];
|
|
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.";
|
|
204
|
+
var optionsSchema = z.array(z.string().min(1)).min(2).max(8).describe("2\u20138 short, distinct choices for the user to pick from. " + WIDGET_LANGUAGE_HINT);
|
|
205
|
+
var TAIL = "Do NOT add an 'Other' or 'None of these' catch-all \u2014 the UI always offers an 'Other' free-text option automatically. " + WIDGET_LANGUAGE_HINT + " After calling this, END YOUR TURN and wait: the user's answer arrives as their next message and starts a fresh run.";
|
|
206
|
+
var singleSelectShape = {
|
|
207
|
+
question: z.string().min(1).describe("The question to ask the user. " + WIDGET_LANGUAGE_HINT),
|
|
208
|
+
body: z.string().optional().describe(
|
|
209
|
+
"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."
|
|
210
|
+
),
|
|
211
|
+
options: optionsSchema
|
|
212
|
+
};
|
|
213
|
+
var multiSelectShape = {
|
|
214
|
+
question: z.string().min(1).describe("The question to ask the user. " + WIDGET_LANGUAGE_HINT),
|
|
215
|
+
body: z.string().optional().describe(
|
|
216
|
+
"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."
|
|
217
|
+
),
|
|
218
|
+
options: optionsSchema
|
|
219
|
+
};
|
|
220
|
+
function buildSingleSelectWidget(args) {
|
|
221
|
+
return {
|
|
222
|
+
id: randomUUID(),
|
|
223
|
+
type: "single_select",
|
|
224
|
+
question: args.question,
|
|
225
|
+
body: args.body,
|
|
226
|
+
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
227
|
+
selectedOptionId: null,
|
|
228
|
+
customAnswer: null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function buildMultiSelectWidget(args) {
|
|
232
|
+
return {
|
|
233
|
+
id: randomUUID(),
|
|
234
|
+
type: "multi_select",
|
|
235
|
+
question: args.question,
|
|
236
|
+
body: args.body,
|
|
237
|
+
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
238
|
+
selectedOptionIds: null,
|
|
239
|
+
customAnswer: null
|
|
240
|
+
};
|
|
241
|
+
}
|
|
200
242
|
function createWidgetTooling() {
|
|
201
243
|
const collected = [];
|
|
202
244
|
const singleSelect = tool(
|
|
203
245
|
SINGLE_SELECT,
|
|
204
246
|
"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
|
-
},
|
|
247
|
+
singleSelectShape,
|
|
212
248
|
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
|
-
});
|
|
249
|
+
collected.push(buildSingleSelectWidget(args));
|
|
222
250
|
return widgetPosted("single-select");
|
|
223
251
|
}
|
|
224
252
|
);
|
|
225
253
|
const multiSelect = tool(
|
|
226
254
|
MULTI_SELECT,
|
|
227
255
|
"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
|
-
},
|
|
256
|
+
multiSelectShape,
|
|
235
257
|
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
|
-
});
|
|
258
|
+
collected.push(buildMultiSelectWidget(args));
|
|
245
259
|
return widgetPosted("multi-select");
|
|
246
260
|
}
|
|
247
261
|
);
|
|
@@ -288,9 +302,6 @@ function langFromPath(path) {
|
|
|
288
302
|
return ext ? EXT_TO_LANG[ext] : void 0;
|
|
289
303
|
}
|
|
290
304
|
|
|
291
|
-
// src/schema-hints.ts
|
|
292
|
-
var INLINE_CODE_HINT = "Wrap code identifiers (function, variable, type, and file names, commands, and flags) in inline backticks, e.g. `getCodingSessionsForRequest`.";
|
|
293
|
-
|
|
294
305
|
// src/plan.ts
|
|
295
306
|
var SERVER_NAME2 = "flume_plan";
|
|
296
307
|
var SUBMIT_PLAN = "submit_plan";
|
|
@@ -318,6 +329,9 @@ var planInputSchema = {
|
|
|
318
329
|
rootCause: z2.string().optional().describe(
|
|
319
330
|
'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
|
|
320
331
|
),
|
|
332
|
+
motivation: z2.string().optional().describe(
|
|
333
|
+
"Why the user is making this request \u2014 the underlying motivation or problem the change addresses. Fill this especially when the request content/context does NOT already state the why (ask the user in the Clarify phase); omit when there is no additional motivation to record. Useful for future understanding of the system. " + INLINE_CODE_HINT
|
|
334
|
+
),
|
|
321
335
|
assumptions: z2.array(z2.string()).describe("Anything decided during planning, including unanswered defaults."),
|
|
322
336
|
requirements: z2.array(z2.string().min(1)).min(1).describe(
|
|
323
337
|
"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
|
|
@@ -348,6 +362,11 @@ function renderPlan(plan) {
|
|
|
348
362
|
lines2.push(`**Scope** \u2014 \`${plan.scope}\``);
|
|
349
363
|
lines2.push("");
|
|
350
364
|
lines2.push(`**Goal** \u2014 ${plan.goal}`);
|
|
365
|
+
if (plan.motivation && plan.motivation.trim().length > 0) {
|
|
366
|
+
lines2.push("");
|
|
367
|
+
lines2.push("## Motivation");
|
|
368
|
+
lines2.push(plan.motivation);
|
|
369
|
+
}
|
|
351
370
|
if (plan.assumptions.length > 0) {
|
|
352
371
|
lines2.push("");
|
|
353
372
|
lines2.push("**Assumptions**");
|
|
@@ -423,7 +442,7 @@ function createPlanTooling() {
|
|
|
423
442
|
let renderedPlans = null;
|
|
424
443
|
const submitPlan = tool2(
|
|
425
444
|
SUBMIT_PLAN,
|
|
426
|
-
`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). `,
|
|
445
|
+
`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). motivation is optional: the user's stated or asked-for reason for the request. `,
|
|
427
446
|
submitPlanInputSchema,
|
|
428
447
|
async (args) => {
|
|
429
448
|
const parsed = submitPlanSchema.parse(args);
|
|
@@ -706,6 +725,87 @@ async function runClaudeCode(opts) {
|
|
|
706
725
|
return { text: finalText, widgets: collected, plans: getPlans(), report: getReport() };
|
|
707
726
|
}
|
|
708
727
|
|
|
728
|
+
// src/codex.ts
|
|
729
|
+
import { spawn } from "node:child_process";
|
|
730
|
+
import { mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
731
|
+
import { join as join2 } from "node:path";
|
|
732
|
+
import { tmpdir } from "node:os";
|
|
733
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
734
|
+
var MCP_ENTRY = fileURLToPath3(new URL("./mcp-stdio.js", import.meta.url));
|
|
735
|
+
async function runCodex(opts) {
|
|
736
|
+
const tmpDir = mkdtempSync(join2(tmpdir(), "flume-codex-"));
|
|
737
|
+
const outFile = join2(tmpDir, "flume-mcp.jsonl");
|
|
738
|
+
writeFileSync2(outFile, "");
|
|
739
|
+
const child = spawn(
|
|
740
|
+
"codex",
|
|
741
|
+
[
|
|
742
|
+
"exec",
|
|
743
|
+
"--cwd",
|
|
744
|
+
opts.cwd,
|
|
745
|
+
"-c",
|
|
746
|
+
`mcp_servers.flume.command=node`,
|
|
747
|
+
"-c",
|
|
748
|
+
`mcp_servers.flume.args=${JSON.stringify([MCP_ENTRY])}`,
|
|
749
|
+
opts.prompt
|
|
750
|
+
],
|
|
751
|
+
{
|
|
752
|
+
cwd: opts.cwd,
|
|
753
|
+
env: { ...process.env, FLUME_MCP_OUTPUT: outFile },
|
|
754
|
+
// stdin is inherited (codex may read from it), stdout/stderr are piped for streaming
|
|
755
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
756
|
+
}
|
|
757
|
+
);
|
|
758
|
+
child.stdout?.on("data", (chunk) => {
|
|
759
|
+
const text = chunk.toString();
|
|
760
|
+
process.stdout.write(text);
|
|
761
|
+
logEvent("agent", text);
|
|
762
|
+
});
|
|
763
|
+
child.stderr?.on("data", (chunk) => {
|
|
764
|
+
const text = chunk.toString();
|
|
765
|
+
process.stderr.write(text);
|
|
766
|
+
logEvent("agent", text);
|
|
767
|
+
});
|
|
768
|
+
const onAbort = () => {
|
|
769
|
+
child.kill("SIGTERM");
|
|
770
|
+
};
|
|
771
|
+
opts.abortController?.signal.addEventListener("abort", onAbort, { once: true });
|
|
772
|
+
await new Promise((resolve, reject) => {
|
|
773
|
+
child.on("error", (err) => {
|
|
774
|
+
if (err.code === "ENOENT") {
|
|
775
|
+
reject(
|
|
776
|
+
new Error(
|
|
777
|
+
"codex CLI not found. Install it with `npm install -g @openai/codex` and log in before running plan-mode jobs with an openai agent."
|
|
778
|
+
)
|
|
779
|
+
);
|
|
780
|
+
} else {
|
|
781
|
+
reject(err);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
child.on("close", () => resolve());
|
|
785
|
+
});
|
|
786
|
+
opts.abortController?.signal.removeEventListener("abort", onAbort);
|
|
787
|
+
if (opts.abortController?.signal.aborted) {
|
|
788
|
+
throw new Error("Run canceled by user");
|
|
789
|
+
}
|
|
790
|
+
const raw = existsSync2(outFile) ? readFileSync3(outFile, "utf8") : "";
|
|
791
|
+
const records = raw.split("\n").filter(Boolean).map(parseJsonLine).filter((r) => r !== null);
|
|
792
|
+
const plans = records.filter((r) => r.kind === "plans").flatMap((r) => r.plans ?? []);
|
|
793
|
+
const widgets = records.filter((r) => r.kind === "widget").map((r) => r.widget);
|
|
794
|
+
return {
|
|
795
|
+
text: "",
|
|
796
|
+
widgets,
|
|
797
|
+
plans: plans.length > 0 ? plans : null,
|
|
798
|
+
report: null
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function parseJsonLine(line) {
|
|
802
|
+
try {
|
|
803
|
+
return JSON.parse(line);
|
|
804
|
+
} catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
709
809
|
// src/health.ts
|
|
710
810
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
711
811
|
var PROBE_TIMEOUT_MS = 6e4;
|
|
@@ -755,24 +855,30 @@ function errorMessage(err) {
|
|
|
755
855
|
}
|
|
756
856
|
|
|
757
857
|
// src/rules.ts
|
|
758
|
-
import { readFileSync as
|
|
759
|
-
import { join as
|
|
760
|
-
import { fileURLToPath as
|
|
761
|
-
var RULES_DIR =
|
|
858
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
859
|
+
import { join as join3 } from "node:path";
|
|
860
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
861
|
+
var RULES_DIR = fileURLToPath4(new URL("../skills-plugin/rules", import.meta.url));
|
|
762
862
|
function loadRule(name) {
|
|
763
|
-
const raw =
|
|
863
|
+
const raw = readFileSync4(join3(RULES_DIR, `${name}.md`), "utf8");
|
|
764
864
|
return stripFrontMatter(raw).trim();
|
|
765
865
|
}
|
|
766
866
|
function stripFrontMatter(raw) {
|
|
767
867
|
const match = raw.match(/^---\n.*?\n---\n/s);
|
|
768
868
|
return match ? raw.slice(match[0].length) : raw;
|
|
769
869
|
}
|
|
870
|
+
var SKILLS_DIR = fileURLToPath4(new URL("../skills-plugin/skills", import.meta.url));
|
|
871
|
+
function loadSkill(name) {
|
|
872
|
+
const raw = readFileSync4(join3(SKILLS_DIR, name, "SKILL.md"), "utf8");
|
|
873
|
+
return stripFrontMatter(raw).trim();
|
|
874
|
+
}
|
|
770
875
|
|
|
771
876
|
// src/prompt.ts
|
|
772
877
|
function appendRule(lines2, intro, ruleName) {
|
|
773
878
|
lines2.push("", intro, "", loadRule(ruleName));
|
|
774
879
|
}
|
|
775
880
|
var WRITING_INTRO = "These technical-writing guidelines apply to the plan and report prose you author in this run:";
|
|
881
|
+
var LANGUAGE_DIRECTIVE = "First, determine the dominant natural language of the incoming thread (the request title/body and the user's messages). Use that one language for EVERYTHING you author this run - your reply body, any plan or report fields, AND every clarifying question and its widget options. Never mix languages: if the thread is in English, your questions and options must be in English too. Keep code identifiers, file paths, and quoted code verbatim.";
|
|
776
882
|
function turnHeading(turn, agentName) {
|
|
777
883
|
if (turn.role === "user") return "User";
|
|
778
884
|
if (turn.failed) return `${agentName} (this run ended in an error)`;
|
|
@@ -796,7 +902,8 @@ function buildPrompt(ctx) {
|
|
|
796
902
|
`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)}.`,
|
|
797
903
|
task,
|
|
798
904
|
orient,
|
|
799
|
-
widgets
|
|
905
|
+
widgets,
|
|
906
|
+
LANGUAGE_DIRECTIVE
|
|
800
907
|
];
|
|
801
908
|
if (ctx.permissionMode !== "plan") {
|
|
802
909
|
lines2.push(
|
|
@@ -828,6 +935,7 @@ function buildRevisePrompt(ctx) {
|
|
|
828
935
|
task,
|
|
829
936
|
orient,
|
|
830
937
|
widgets,
|
|
938
|
+
LANGUAGE_DIRECTIVE,
|
|
831
939
|
"",
|
|
832
940
|
"These coding guidelines apply to all code produced in this run:",
|
|
833
941
|
"",
|
|
@@ -943,6 +1051,7 @@ function buildReleasePrompt(ctx, baseChecks) {
|
|
|
943
1051
|
task,
|
|
944
1052
|
orient,
|
|
945
1053
|
widgets,
|
|
1054
|
+
LANGUAGE_DIRECTIVE,
|
|
946
1055
|
"",
|
|
947
1056
|
"These coding guidelines apply to all code produced in this run:",
|
|
948
1057
|
"",
|
|
@@ -970,6 +1079,14 @@ function buildReleasePrompt(ctx, baseChecks) {
|
|
|
970
1079
|
"```"
|
|
971
1080
|
);
|
|
972
1081
|
}
|
|
1082
|
+
if (ctx.prerelease) {
|
|
1083
|
+
lines2.push(
|
|
1084
|
+
"",
|
|
1085
|
+
"# Pre-release",
|
|
1086
|
+
"",
|
|
1087
|
+
"This is a PRE-RELEASE. When proposing and applying versions, use a semver pre-release version string (e.g. `0.9.0-beta.1`): take the next stable version you would otherwise pick and append `-beta.N`, where N is the next unused beta number for that version (check existing `v<version>-beta.*` tags). Offer these pre-release strings in the version-confirmation widgets, and write them to package.json, CHANGELOG.md, and the `flumecode:versions` comment as usual."
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
973
1090
|
appendThread(lines2, ctx);
|
|
974
1091
|
lines2.push(
|
|
975
1092
|
"",
|
|
@@ -977,6 +1094,12 @@ function buildReleasePrompt(ctx, baseChecks) {
|
|
|
977
1094
|
);
|
|
978
1095
|
return lines2.join("\n");
|
|
979
1096
|
}
|
|
1097
|
+
function buildCodexPrompt(ctx) {
|
|
1098
|
+
const skill = loadSkill("request-to-plan");
|
|
1099
|
+
return ["# request-to-plan skill (follow these instructions)", skill, "", buildPrompt(ctx)].join(
|
|
1100
|
+
"\n"
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
980
1103
|
function buildInitPrompt(ctx) {
|
|
981
1104
|
return [
|
|
982
1105
|
`You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
@@ -993,10 +1116,10 @@ function jobTitle(ctx) {
|
|
|
993
1116
|
|
|
994
1117
|
// src/workspace.ts
|
|
995
1118
|
import { execFile } from "node:child_process";
|
|
996
|
-
import { existsSync as
|
|
1119
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
997
1120
|
import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
998
|
-
import { tmpdir } from "node:os";
|
|
999
|
-
import { join as
|
|
1121
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1122
|
+
import { join as join4 } from "node:path";
|
|
1000
1123
|
import { promisify } from "node:util";
|
|
1001
1124
|
var exec = promisify(execFile);
|
|
1002
1125
|
var WORKSPACE_PREFIX = "flume-runner-";
|
|
@@ -1022,11 +1145,11 @@ function cloneUrl(ctx) {
|
|
|
1022
1145
|
return `https://x-access-token:${cloneToken}@github.com/${owner}/${name}.git`;
|
|
1023
1146
|
}
|
|
1024
1147
|
function detectPackageManager(dir) {
|
|
1025
|
-
if (!
|
|
1026
|
-
if (
|
|
1027
|
-
if (
|
|
1028
|
-
if (
|
|
1029
|
-
if (
|
|
1148
|
+
if (!existsSync3(join4(dir, "package.json"))) return null;
|
|
1149
|
+
if (existsSync3(join4(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1150
|
+
if (existsSync3(join4(dir, "yarn.lock"))) return "yarn";
|
|
1151
|
+
if (existsSync3(join4(dir, "package-lock.json"))) return "npm";
|
|
1152
|
+
if (existsSync3(join4(dir, "bun.lockb"))) return "bun";
|
|
1030
1153
|
return "npm";
|
|
1031
1154
|
}
|
|
1032
1155
|
async function installDependencies(dir) {
|
|
@@ -1052,13 +1175,13 @@ async function installDependencies(dir) {
|
|
|
1052
1175
|
}
|
|
1053
1176
|
}
|
|
1054
1177
|
async function makeWorkspace() {
|
|
1055
|
-
return mkdtemp(
|
|
1178
|
+
return mkdtemp(join4(tmpdir2(), WORKSPACE_PREFIX));
|
|
1056
1179
|
}
|
|
1057
1180
|
var MAX_WORKSPACES = 8;
|
|
1058
1181
|
var workspaceRegistry = /* @__PURE__ */ new Map();
|
|
1059
1182
|
async function acquireWorkspace(key) {
|
|
1060
1183
|
const existing = workspaceRegistry.get(key);
|
|
1061
|
-
if (existing !== void 0 &&
|
|
1184
|
+
if (existing !== void 0 && existsSync3(existing)) {
|
|
1062
1185
|
workspaceRegistry.delete(key);
|
|
1063
1186
|
workspaceRegistry.set(key, existing);
|
|
1064
1187
|
return { dir: existing, reused: true };
|
|
@@ -1110,7 +1233,7 @@ async function prepareResumingBranch(ctx, dir, reused) {
|
|
|
1110
1233
|
return { resumed: true };
|
|
1111
1234
|
}
|
|
1112
1235
|
async function sweepWorkspaces() {
|
|
1113
|
-
const base =
|
|
1236
|
+
const base = tmpdir2();
|
|
1114
1237
|
let entries;
|
|
1115
1238
|
try {
|
|
1116
1239
|
entries = await readdir(base);
|
|
@@ -1121,7 +1244,7 @@ async function sweepWorkspaces() {
|
|
|
1121
1244
|
for (const entry of entries) {
|
|
1122
1245
|
if (!entry.startsWith(WORKSPACE_PREFIX)) continue;
|
|
1123
1246
|
try {
|
|
1124
|
-
await rm(
|
|
1247
|
+
await rm(join4(base, entry), { recursive: true, force: true });
|
|
1125
1248
|
removed++;
|
|
1126
1249
|
} catch {
|
|
1127
1250
|
}
|
|
@@ -1518,8 +1641,14 @@ async function processChatJob(ctx, dir, config, abort) {
|
|
|
1518
1641
|
console.log(`
|
|
1519
1642
|
\u25B6 Job ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1520
1643
|
const orchestrating = ctx.permissionMode !== "plan";
|
|
1644
|
+
const provider = ctx.provider ?? "anthropic";
|
|
1645
|
+
if (provider === "openai" && orchestrating) {
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
"Codex backend currently supports plan mode only. implement/revise/resolve/release jobs with an openai agent are not yet supported."
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1521
1650
|
const installResult = orchestrating ? await installDependencies(dir) : null;
|
|
1522
|
-
const result = await runClaudeCode({
|
|
1651
|
+
const result = provider === "openai" ? await runCodex({ cwd: dir, prompt: buildCodexPrompt(ctx), abortController: abort }) : await runClaudeCode({
|
|
1523
1652
|
cwd: dir,
|
|
1524
1653
|
prompt: buildPrompt(ctx),
|
|
1525
1654
|
permissionMode: ctx.permissionMode,
|
|
@@ -1540,7 +1669,7 @@ async function processChatJob(ctx, dir, config, abort) {
|
|
|
1540
1669
|
console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
|
|
1541
1670
|
return { text: reply, widgets: result.widgets };
|
|
1542
1671
|
}
|
|
1543
|
-
const wikiExists =
|
|
1672
|
+
const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
|
|
1544
1673
|
let documented = false;
|
|
1545
1674
|
if (ctx.permissionMode !== "plan" && wikiExists && await hasChanges(dir)) {
|
|
1546
1675
|
try {
|
|
@@ -1629,7 +1758,7 @@ ${reply}`;
|
|
|
1629
1758
|
|
|
1630
1759
|
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
1631
1760
|
}
|
|
1632
|
-
const wikiExists =
|
|
1761
|
+
const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
|
|
1633
1762
|
let documented = false;
|
|
1634
1763
|
if (wikiExists && await hasChanges(dir)) {
|
|
1635
1764
|
try {
|
|
@@ -1685,7 +1814,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
|
|
|
1685
1814
|
console.log(` \u2026revise ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
|
|
1686
1815
|
return { text: reply, widgets: result.widgets };
|
|
1687
1816
|
}
|
|
1688
|
-
const wikiExists =
|
|
1817
|
+
const wikiExists = existsSync4(join5(dir, ".flumecode", "wiki"));
|
|
1689
1818
|
let documented = false;
|
|
1690
1819
|
if (wikiExists && await hasChanges(dir)) {
|
|
1691
1820
|
try {
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/mcp-stdio.ts
|
|
5
|
+
import { appendFileSync, writeFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { toJSONSchema } from "zod/v4/core";
|
|
10
|
+
import { z as z3 } from "zod";
|
|
11
|
+
|
|
12
|
+
// src/plan.ts
|
|
13
|
+
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/code-lang.ts
|
|
17
|
+
var EXT_TO_LANG = {
|
|
18
|
+
ts: "typescript",
|
|
19
|
+
tsx: "tsx",
|
|
20
|
+
js: "javascript",
|
|
21
|
+
jsx: "jsx",
|
|
22
|
+
json: "json",
|
|
23
|
+
css: "css",
|
|
24
|
+
md: "markdown",
|
|
25
|
+
sh: "bash",
|
|
26
|
+
py: "python",
|
|
27
|
+
yaml: "yaml",
|
|
28
|
+
yml: "yaml",
|
|
29
|
+
html: "markup",
|
|
30
|
+
xml: "markup",
|
|
31
|
+
sql: "sql"
|
|
32
|
+
};
|
|
33
|
+
function langFromPath(path) {
|
|
34
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
35
|
+
return ext ? EXT_TO_LANG[ext] : void 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/schema-hints.ts
|
|
39
|
+
var INLINE_CODE_HINT = "Wrap code identifiers (function, variable, type, and file names, commands, and flags) in inline backticks, e.g. `getCodingSessionsForRequest`.";
|
|
40
|
+
var WIDGET_LANGUAGE_HINT = "Write this in the same natural language as the incoming thread (the request body and the user's messages). If the thread is in English, keep it in English; do not switch languages. Keep code identifiers, file paths, and quoted code verbatim.";
|
|
41
|
+
|
|
42
|
+
// src/plan.ts
|
|
43
|
+
var SERVER_NAME = "flume_plan";
|
|
44
|
+
var SUBMIT_PLAN = "submit_plan";
|
|
45
|
+
var PLAN_TOOL_NAME = `mcp__${SERVER_NAME}__${SUBMIT_PLAN}`;
|
|
46
|
+
var PLAN_MARKER = "<!-- flumecode:end-of-plan -->";
|
|
47
|
+
var pseudoCodeEntrySchema = z.object({
|
|
48
|
+
file: z.string().min(1),
|
|
49
|
+
pseudoCode: z.string().min(1)
|
|
50
|
+
});
|
|
51
|
+
var stepSchema = z.object({
|
|
52
|
+
title: z.string().min(1).describe("A concise imperative title for this step."),
|
|
53
|
+
description: z.array(z.string().min(1)).min(1).describe(
|
|
54
|
+
"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
|
|
55
|
+
),
|
|
56
|
+
pseudoCode: z.array(pseudoCodeEntrySchema).optional().describe(
|
|
57
|
+
"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."
|
|
58
|
+
)
|
|
59
|
+
});
|
|
60
|
+
var planInputSchema = {
|
|
61
|
+
title: z.string().min(1).max(120).describe(
|
|
62
|
+
"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."
|
|
63
|
+
),
|
|
64
|
+
scope: z.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
|
|
65
|
+
goal: z.string().min(1).describe("One or two sentences stating the outcome. " + INLINE_CODE_HINT),
|
|
66
|
+
rootCause: z.string().optional().describe(
|
|
67
|
+
'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
|
|
68
|
+
),
|
|
69
|
+
motivation: z.string().optional().describe(
|
|
70
|
+
"Why the user is making this request \u2014 the underlying motivation or problem the change addresses. Fill this especially when the request content/context does NOT already state the why (ask the user in the Clarify phase); omit when there is no additional motivation to record. Useful for future understanding of the system. " + INLINE_CODE_HINT
|
|
71
|
+
),
|
|
72
|
+
assumptions: z.array(z.string()).describe("Anything decided during planning, including unanswered defaults."),
|
|
73
|
+
requirements: z.array(z.string().min(1)).min(1).describe(
|
|
74
|
+
"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
|
|
75
|
+
),
|
|
76
|
+
steps: z.array(stepSchema).min(1).describe("Ordered list of changes. Each step says what and why, with file references."),
|
|
77
|
+
acceptanceCriteria: z.array(z.string().min(1)).min(2).describe(
|
|
78
|
+
"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
|
|
79
|
+
),
|
|
80
|
+
risks: z.array(z.string()).describe("Anything that could change the approach."),
|
|
81
|
+
outOfScope: z.array(z.string()).describe("What is deliberately not being done.")
|
|
82
|
+
};
|
|
83
|
+
function requireRootCauseForFix(schema) {
|
|
84
|
+
return schema.superRefine((plan, ctx) => {
|
|
85
|
+
if (plan.scope === "fix" && (plan.rootCause === void 0 || plan.rootCause.trim() === "")) {
|
|
86
|
+
ctx.addIssue({
|
|
87
|
+
code: z.ZodIssueCode.custom,
|
|
88
|
+
path: ["rootCause"],
|
|
89
|
+
message: 'rootCause is required and must be non-empty when scope is "fix".'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
var planSchema = requireRootCauseForFix(z.object(planInputSchema));
|
|
95
|
+
function renderPlan(plan) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
lines.push(`# ${plan.title}`);
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push(`**Scope** \u2014 \`${plan.scope}\``);
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push(`**Goal** \u2014 ${plan.goal}`);
|
|
102
|
+
if (plan.motivation && plan.motivation.trim().length > 0) {
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push("## Motivation");
|
|
105
|
+
lines.push(plan.motivation);
|
|
106
|
+
}
|
|
107
|
+
if (plan.assumptions.length > 0) {
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push("**Assumptions**");
|
|
110
|
+
for (const assumption of plan.assumptions) {
|
|
111
|
+
lines.push(`- ${assumption}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (plan.rootCause && plan.rootCause.trim().length > 0) {
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push("## Root cause");
|
|
117
|
+
lines.push(plan.rootCause);
|
|
118
|
+
}
|
|
119
|
+
lines.push("");
|
|
120
|
+
lines.push("## Requirements");
|
|
121
|
+
for (const requirement of plan.requirements) {
|
|
122
|
+
lines.push(`- ${requirement}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push("## Steps");
|
|
126
|
+
for (const [i, step] of plan.steps.entries()) {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(`### ${i + 1}. ${step.title}`);
|
|
129
|
+
lines.push("");
|
|
130
|
+
for (const bullet of step.description) {
|
|
131
|
+
lines.push(`- ${bullet}`);
|
|
132
|
+
}
|
|
133
|
+
if (step.pseudoCode && step.pseudoCode.length > 0) {
|
|
134
|
+
for (const entry of step.pseudoCode) {
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push(`\`${entry.file}\``);
|
|
137
|
+
lines.push("");
|
|
138
|
+
const lang = langFromPath(entry.file);
|
|
139
|
+
lines.push(lang ? "```" + lang : "```");
|
|
140
|
+
lines.push(entry.pseudoCode);
|
|
141
|
+
lines.push("```");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push("## Acceptance criteria");
|
|
147
|
+
for (const criterion of plan.acceptanceCriteria) {
|
|
148
|
+
lines.push(`- [ ] ${criterion}`);
|
|
149
|
+
}
|
|
150
|
+
if (plan.risks.length > 0) {
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push("**Risks / open questions**");
|
|
153
|
+
for (const risk of plan.risks) {
|
|
154
|
+
lines.push(`- ${risk}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (plan.outOfScope.length > 0) {
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push("**Out of scope**");
|
|
160
|
+
for (const item of plan.outOfScope) {
|
|
161
|
+
lines.push(`- ${item}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push(PLAN_MARKER);
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
var submitPlanInputSchema = {
|
|
169
|
+
plans: z.array(requireRootCauseForFix(z.object(planInputSchema))).min(1).refine(
|
|
170
|
+
(arr) => {
|
|
171
|
+
const titles = arr.map((p) => p.title.trim()).filter((t) => t.length > 0);
|
|
172
|
+
return new Set(titles).size === titles.length;
|
|
173
|
+
},
|
|
174
|
+
{ message: "Each plan must have a distinct non-empty title" }
|
|
175
|
+
)
|
|
176
|
+
};
|
|
177
|
+
var submitPlanSchema = z.object(submitPlanInputSchema);
|
|
178
|
+
|
|
179
|
+
// src/widgets.ts
|
|
180
|
+
import { randomUUID } from "node:crypto";
|
|
181
|
+
import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
182
|
+
import { z as z2 } from "zod";
|
|
183
|
+
var SERVER_NAME2 = "flume_widgets";
|
|
184
|
+
var SINGLE_SELECT = "single_select";
|
|
185
|
+
var MULTI_SELECT = "multi_select";
|
|
186
|
+
var WIDGET_TOOL_NAMES = [
|
|
187
|
+
`mcp__${SERVER_NAME2}__${SINGLE_SELECT}`,
|
|
188
|
+
`mcp__${SERVER_NAME2}__${MULTI_SELECT}`
|
|
189
|
+
];
|
|
190
|
+
var optionsSchema = z2.array(z2.string().min(1)).min(2).max(8).describe("2\u20138 short, distinct choices for the user to pick from. " + WIDGET_LANGUAGE_HINT);
|
|
191
|
+
var TAIL = "Do NOT add an 'Other' or 'None of these' catch-all \u2014 the UI always offers an 'Other' free-text option automatically. " + WIDGET_LANGUAGE_HINT + " After calling this, END YOUR TURN and wait: the user's answer arrives as their next message and starts a fresh run.";
|
|
192
|
+
var singleSelectShape = {
|
|
193
|
+
question: z2.string().min(1).describe("The question to ask the user. " + WIDGET_LANGUAGE_HINT),
|
|
194
|
+
body: z2.string().optional().describe(
|
|
195
|
+
"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."
|
|
196
|
+
),
|
|
197
|
+
options: optionsSchema
|
|
198
|
+
};
|
|
199
|
+
var multiSelectShape = {
|
|
200
|
+
question: z2.string().min(1).describe("The question to ask the user. " + WIDGET_LANGUAGE_HINT),
|
|
201
|
+
body: z2.string().optional().describe(
|
|
202
|
+
"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."
|
|
203
|
+
),
|
|
204
|
+
options: optionsSchema
|
|
205
|
+
};
|
|
206
|
+
function buildSingleSelectWidget(args) {
|
|
207
|
+
return {
|
|
208
|
+
id: randomUUID(),
|
|
209
|
+
type: "single_select",
|
|
210
|
+
question: args.question,
|
|
211
|
+
body: args.body,
|
|
212
|
+
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
213
|
+
selectedOptionId: null,
|
|
214
|
+
customAnswer: null
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function buildMultiSelectWidget(args) {
|
|
218
|
+
return {
|
|
219
|
+
id: randomUUID(),
|
|
220
|
+
type: "multi_select",
|
|
221
|
+
question: args.question,
|
|
222
|
+
body: args.body,
|
|
223
|
+
options: args.options.map((label) => ({ id: randomUUID(), label })),
|
|
224
|
+
selectedOptionIds: null,
|
|
225
|
+
customAnswer: null
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/mcp-stdio.ts
|
|
230
|
+
var OUT = process.env.FLUME_MCP_OUTPUT;
|
|
231
|
+
if (!OUT) {
|
|
232
|
+
console.error("FLUME_MCP_OUTPUT env var not set");
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
if (!existsSync(OUT)) {
|
|
236
|
+
writeFileSync(OUT, "");
|
|
237
|
+
}
|
|
238
|
+
function emit(rec) {
|
|
239
|
+
appendFileSync(OUT, JSON.stringify(rec) + "\n");
|
|
240
|
+
}
|
|
241
|
+
function ack(text) {
|
|
242
|
+
return { content: [{ type: "text", text }] };
|
|
243
|
+
}
|
|
244
|
+
function shapeToJsonSchema(shape) {
|
|
245
|
+
return toJSONSchema(z3.object(shape));
|
|
246
|
+
}
|
|
247
|
+
var TOOLS = [
|
|
248
|
+
{
|
|
249
|
+
name: "submit_plan",
|
|
250
|
+
description: "Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable plan draft. Do NOT call submit_plan more than once.",
|
|
251
|
+
inputSchema: toJSONSchema(submitPlanSchema)
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "single_select",
|
|
255
|
+
description: "Ask the user a single-select (radio-button) question \u2014 exactly one answer.",
|
|
256
|
+
inputSchema: shapeToJsonSchema(singleSelectShape)
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "multi_select",
|
|
260
|
+
description: "Ask the user a multi-select (checkbox) question \u2014 they may pick any number of options.",
|
|
261
|
+
inputSchema: shapeToJsonSchema(multiSelectShape)
|
|
262
|
+
}
|
|
263
|
+
];
|
|
264
|
+
var server = new Server({ name: "flume", version: "1" }, { capabilities: { tools: {} } });
|
|
265
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
266
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
267
|
+
const { name, arguments: args } = request.params;
|
|
268
|
+
if (name === "submit_plan") {
|
|
269
|
+
const parsed = submitPlanSchema.parse(args);
|
|
270
|
+
emit({ kind: "plans", plans: parsed.plans.map(renderPlan) });
|
|
271
|
+
return ack("Plan(s) submitted. End your turn.");
|
|
272
|
+
}
|
|
273
|
+
if (name === "single_select") {
|
|
274
|
+
const parsed = z3.object(singleSelectShape).parse(args);
|
|
275
|
+
emit({ kind: "widget", widget: buildSingleSelectWidget(parsed) });
|
|
276
|
+
return ack("Question posted as a single-select widget. End your turn.");
|
|
277
|
+
}
|
|
278
|
+
if (name === "multi_select") {
|
|
279
|
+
const parsed = z3.object(multiSelectShape).parse(args);
|
|
280
|
+
emit({ kind: "widget", widget: buildMultiSelectWidget(parsed) });
|
|
281
|
+
return ack("Question posted as a multi-select widget. End your turn.");
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
284
|
+
});
|
|
285
|
+
var transport = new StdioServerTransport();
|
|
286
|
+
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flumecode/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "FlumeCode local runner — claims jobs and drives your local Claude Code against a real checkout.",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"provenance": false
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "esbuild src/cli.ts --bundle --platform=node --format=esm --target=node20 --packages=external --
|
|
21
|
+
"build": "esbuild src/cli.ts src/mcp-stdio.ts --bundle --platform=node --format=esm --target=node20 --packages=external --outdir=dist --banner:js=\"#!/usr/bin/env node\"",
|
|
22
22
|
"dev": "tsx src/cli.ts",
|
|
23
23
|
"login": "tsx src/cli.ts login",
|
|
24
24
|
"start": "tsx src/cli.ts start",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@anthropic-ai/claude-agent-sdk": "^0.3.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1",
|
|
30
31
|
"zod": "4.4.3"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
@@ -16,9 +16,9 @@ This convention applies to all free-text fields in plans and reports: goals, ste
|
|
|
16
16
|
|
|
17
17
|
## Output language
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
commands, and quoted code/diffs verbatim; only the surrounding prose follows
|
|
24
|
-
|
|
19
|
+
Before writing anything, determine the dominant natural language of the incoming thread (the
|
|
20
|
+
request title/body and the user's messages). Use that one language for all free-text prose in
|
|
21
|
+
this run — your reply body, plan goals/steps/risks, report summaries, clarifying questions,
|
|
22
|
+
widget options, and push-backs. Never switch languages mid-response. Keep code identifiers, file
|
|
23
|
+
paths, commands, and quoted code/diffs verbatim; only the surrounding prose follows the thread
|
|
24
|
+
language.
|
|
@@ -183,6 +183,32 @@ version did not change.
|
|
|
183
183
|
silence them.
|
|
184
184
|
- **Never commit, push, or open a PR** — the runner does that.
|
|
185
185
|
|
|
186
|
+
## Pre-release
|
|
187
|
+
|
|
188
|
+
When the prompt contains a `# Pre-release` section, this release uses semver
|
|
189
|
+
pre-release version strings instead of stable ones:
|
|
190
|
+
|
|
191
|
+
- **Compute versions:** take the next stable version you would otherwise propose
|
|
192
|
+
(patch or minor bump), then append `-beta.N`, where N is the next unused beta
|
|
193
|
+
number for that base version. Check existing tags with:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
git tag -l --sort=-version:refname 'v<version>-beta.*' | head -1
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If no beta tags exist for that base version, start at `-beta.1`.
|
|
200
|
+
|
|
201
|
+
- **Phase 1 (propose):** offer the pre-release version string (e.g.
|
|
202
|
+
`0.9.0-beta.1`) in the version-confirmation widgets instead of the stable
|
|
203
|
+
version.
|
|
204
|
+
|
|
205
|
+
- **Phase 2 (apply):** write the pre-release version string (e.g.
|
|
206
|
+
`0.9.0-beta.1`) to `package.json`, `CHANGELOG.md`, and the
|
|
207
|
+
`<!-- flumecode:versions {...} -->` comment — exactly as you would for a
|
|
208
|
+
stable release, just with the pre-release suffix included.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
186
212
|
## Pre-release checks
|
|
187
213
|
|
|
188
214
|
We cannot release code with failing checks. Before this turn, the runner ran the
|
|
@@ -34,7 +34,10 @@ and determine which phase you are in:**
|
|
|
34
34
|
If the request is a bug fix, investigate deeply enough to identify the **root cause** (the specific code or condition producing the incorrect behavior) before proceeding to Plan — you will need it for the `rootCause` field.
|
|
35
35
|
2. Identify genuine ambiguity only: scope, intended behavior, edge cases,
|
|
36
36
|
competing approaches, acceptance criteria. Skip anything you can reasonably
|
|
37
|
-
decide yourself.
|
|
37
|
+
decide yourself. One high-leverage question is the user's motivation — **why** they
|
|
38
|
+
are making this request — but only when the request title/body and context don't
|
|
39
|
+
already explain it. Never re-ask what the thread already answers (consistent with
|
|
40
|
+
"Never ask what the code answers").
|
|
38
41
|
3. End your turn with **2–5 specific, high-leverage questions**, each with a
|
|
39
42
|
recommended default so the user can reply "all defaults" if they want. Group
|
|
40
43
|
them; keep it short. Do **not** include a plan yet.
|
|
@@ -66,6 +69,7 @@ Field-by-field guidance:
|
|
|
66
69
|
answers the request's title and body. Must be achievable by the steps below
|
|
67
70
|
and nothing more.
|
|
68
71
|
- **`rootCause`** — required when `scope` is `fix`; omit for all other scopes. Identify the underlying cause of the bug — the specific code, logic, or condition that produces the incorrect behavior, **not** the symptom. To fill this accurately, you must investigate the codebase deeply enough to find the root cause before writing the plan.
|
|
72
|
+
- **`motivation`** — optional. The user's stated or asked-for reason for making this request — the underlying motivation or problem the change addresses. Fill this when the request content/context does NOT already state the why (ask during Phase 1 — Clarify if needed); omit when there is no additional motivation to record. Useful for future understanding of the system.
|
|
69
73
|
- **`assumptions`** — anything you decided during investigation (including
|
|
70
74
|
unanswered defaults from Phase 1).
|
|
71
75
|
- **`requirements`** — **required; at least 1 item.** Plain-language statements of what this change must accomplish and why, written so a non-technical reader can follow them. Distinct from `acceptanceCriteria`: requirements explain intent and rationale; acceptance criteria are the machine-checkable proof. At least 1 item required.
|
|
@@ -108,6 +112,13 @@ own independently-acceptable "Accept as plan" draft. After a plan is accepted th
|
|
|
108
112
|
keep commenting to refine it; treat a later turn as a fresh **Plan** phase and call
|
|
109
113
|
`submit_plan` again with a `plans[]` array containing the revised fields.
|
|
110
114
|
|
|
115
|
+
Before adding an entry to `plans[]`, apply this right-sizing checklist — if a plan fails any criterion, split it into separate entries:
|
|
116
|
+
|
|
117
|
+
- **Single, clear outcome** — one bug fixed, one feature increment, one refactor. If the `title` needs "and", consider splitting.
|
|
118
|
+
- **Fits in a sprint comfortably** — if it can't fit in one iteration, it's likely an epic that needs breaking down.
|
|
119
|
+
- **Reviewable PR** — small enough that a reviewer can hold it in their head (often cited as under ~200–400 lines of diff, though this varies).
|
|
120
|
+
- **Testable acceptance criteria** — you can state up front what "done" looks like; use the `acceptanceCriteria` field to capture this.
|
|
121
|
+
|
|
111
122
|
## Always
|
|
112
123
|
|
|
113
124
|
- Stay read-only. Propose; do not edit.
|