@flumecode/runner 0.3.0 → 0.3.2
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
|
@@ -39,8 +39,48 @@ function readVersion() {
|
|
|
39
39
|
}
|
|
40
40
|
var RUNNER_VERSION = readVersion();
|
|
41
41
|
var RUNNER_VERSION_HEADER = "x-flumecode-runner-version";
|
|
42
|
+
var RUNNER_MIN_VERSION_HEADER = "x-flumecode-min-runner-version";
|
|
43
|
+
var RUNNER_LATEST_VERSION_HEADER = "x-flumecode-latest-runner-version";
|
|
44
|
+
function compareVersions(a, b) {
|
|
45
|
+
const parse = (v) => (v.split("-")[0] ?? v).split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
46
|
+
const pa = parse(a);
|
|
47
|
+
const pb = parse(b);
|
|
48
|
+
const len = Math.max(pa.length, pb.length);
|
|
49
|
+
for (let i = 0; i < len; i++) {
|
|
50
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
51
|
+
if (diff !== 0) return diff;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
42
55
|
|
|
43
56
|
// src/api.ts
|
|
57
|
+
var OUTDATED_WARN_INTERVAL_MS = 60 * 6e4;
|
|
58
|
+
var lastOutdatedWarnAt = 0;
|
|
59
|
+
var UPDATE_NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
60
|
+
var lastUpdateNudgeAt = 0;
|
|
61
|
+
function noteServerVersion(res) {
|
|
62
|
+
const min = res.headers.get(RUNNER_MIN_VERSION_HEADER)?.trim();
|
|
63
|
+
if (min && RUNNER_VERSION !== "unknown" && compareVersions(RUNNER_VERSION, min) < 0) {
|
|
64
|
+
const now2 = Date.now();
|
|
65
|
+
if (now2 - lastOutdatedWarnAt >= OUTDATED_WARN_INTERVAL_MS) {
|
|
66
|
+
lastOutdatedWarnAt = now2;
|
|
67
|
+
console.warn(
|
|
68
|
+
`\u26A0\uFE0F This runner (v${RUNNER_VERSION}) is outdated \u2014 the server expects v${min} or newer.
|
|
69
|
+
Update with: npm install -g @flumecode/runner@latest`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const latest = res.headers.get(RUNNER_LATEST_VERSION_HEADER)?.trim();
|
|
75
|
+
if (!latest || RUNNER_VERSION === "unknown") return;
|
|
76
|
+
if (compareVersions(RUNNER_VERSION, latest) >= 0) return;
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (now - lastUpdateNudgeAt < UPDATE_NUDGE_INTERVAL_MS) return;
|
|
79
|
+
lastUpdateNudgeAt = now;
|
|
80
|
+
console.log(
|
|
81
|
+
`\u2139\uFE0F A newer runner is available (v${latest}; you have v${RUNNER_VERSION}). Update with: npm install -g @flumecode/runner@latest`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
44
84
|
async function claimJob(config) {
|
|
45
85
|
const res = await fetch(`${config.serverUrl}/api/runner/jobs/claim`, {
|
|
46
86
|
method: "POST",
|
|
@@ -49,6 +89,7 @@ async function claimJob(config) {
|
|
|
49
89
|
[RUNNER_VERSION_HEADER]: RUNNER_VERSION
|
|
50
90
|
}
|
|
51
91
|
});
|
|
92
|
+
noteServerVersion(res);
|
|
52
93
|
if (res.status === 204) return null;
|
|
53
94
|
if (res.status === 401) {
|
|
54
95
|
throw new Error("Runner token rejected (401). Re-run `login` with a fresh token.");
|
|
@@ -66,6 +107,7 @@ async function reportJob(config, jobId, result) {
|
|
|
66
107
|
},
|
|
67
108
|
body: JSON.stringify(result)
|
|
68
109
|
});
|
|
110
|
+
noteServerVersion(res);
|
|
69
111
|
if (!res.ok) throw new Error(`complete failed: ${res.status} ${await safeText(res)}`);
|
|
70
112
|
}
|
|
71
113
|
async function reportHeartbeat(config, claudeCode) {
|
|
@@ -78,6 +120,7 @@ async function reportHeartbeat(config, claudeCode) {
|
|
|
78
120
|
},
|
|
79
121
|
body: JSON.stringify({ claudeCode })
|
|
80
122
|
});
|
|
123
|
+
noteServerVersion(res);
|
|
81
124
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status} ${await safeText(res)}`);
|
|
82
125
|
}
|
|
83
126
|
async function safeText(res) {
|
|
@@ -236,19 +279,30 @@ function renderPlan(plan) {
|
|
|
236
279
|
lines.push(PLAN_MARKER);
|
|
237
280
|
return lines.join("\n");
|
|
238
281
|
}
|
|
282
|
+
var submitPlanInputSchema = {
|
|
283
|
+
plans: z2.array(z2.object(planInputSchema)).min(1).refine(
|
|
284
|
+
(arr) => {
|
|
285
|
+
const titles = arr.map((p) => p.title.trim()).filter((t) => t.length > 0);
|
|
286
|
+
return new Set(titles).size === titles.length;
|
|
287
|
+
},
|
|
288
|
+
{ message: "Each plan must have a distinct non-empty title" }
|
|
289
|
+
)
|
|
290
|
+
};
|
|
291
|
+
var submitPlanSchema = z2.object(submitPlanInputSchema);
|
|
239
292
|
function createPlanTooling() {
|
|
240
|
-
let
|
|
293
|
+
let renderedPlans = null;
|
|
241
294
|
const submitPlan = tool2(
|
|
242
295
|
SUBMIT_PLAN,
|
|
243
|
-
"Submit
|
|
244
|
-
|
|
296
|
+
"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.",
|
|
297
|
+
submitPlanInputSchema,
|
|
245
298
|
async (args) => {
|
|
246
|
-
|
|
299
|
+
const parsed = submitPlanSchema.parse(args);
|
|
300
|
+
renderedPlans = parsed.plans.map(renderPlan);
|
|
247
301
|
return {
|
|
248
302
|
content: [
|
|
249
303
|
{
|
|
250
304
|
type: "text",
|
|
251
|
-
text: "Plan submitted. The runner will render and post
|
|
305
|
+
text: "Plan(s) submitted. The runner will render and post them as your comment(s). End your turn now."
|
|
252
306
|
}
|
|
253
307
|
]
|
|
254
308
|
};
|
|
@@ -258,7 +312,7 @@ function createPlanTooling() {
|
|
|
258
312
|
name: SERVER_NAME2,
|
|
259
313
|
tools: [submitPlan]
|
|
260
314
|
});
|
|
261
|
-
return { mcpServer,
|
|
315
|
+
return { mcpServer, getPlans: () => renderedPlans };
|
|
262
316
|
}
|
|
263
317
|
|
|
264
318
|
// src/executor.ts
|
|
@@ -266,7 +320,7 @@ var FLUME_PLUGIN_DIR = fileURLToPath2(new URL("../skills-plugin", import.meta.ur
|
|
|
266
320
|
async function runClaudeCode(opts) {
|
|
267
321
|
let finalText = "";
|
|
268
322
|
const { mcpServer, collected } = createWidgetTooling();
|
|
269
|
-
const { mcpServer: planServer,
|
|
323
|
+
const { mcpServer: planServer, getPlans } = createPlanTooling();
|
|
270
324
|
for await (const message of query({
|
|
271
325
|
prompt: opts.prompt,
|
|
272
326
|
options: {
|
|
@@ -304,7 +358,7 @@ async function runClaudeCode(opts) {
|
|
|
304
358
|
}
|
|
305
359
|
}
|
|
306
360
|
process.stdout.write("\n");
|
|
307
|
-
return { text: finalText, widgets: collected,
|
|
361
|
+
return { text: finalText, widgets: collected, plans: getPlans() };
|
|
308
362
|
}
|
|
309
363
|
|
|
310
364
|
// src/health.ts
|
|
@@ -828,14 +882,14 @@ async function processChatJob(ctx, dir) {
|
|
|
828
882
|
});
|
|
829
883
|
const summary = result.text.trim();
|
|
830
884
|
let reply = summary || "(the agent produced no summary)";
|
|
831
|
-
if (result.plan) {
|
|
832
|
-
reply = result.plan;
|
|
833
|
-
}
|
|
834
885
|
if (installResult?.status === "failed") {
|
|
835
886
|
reply += `
|
|
836
887
|
|
|
837
888
|
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
838
889
|
}
|
|
890
|
+
if (result.plans?.length) {
|
|
891
|
+
return { text: result.text.trim(), widgets: [], plans: result.plans };
|
|
892
|
+
}
|
|
839
893
|
if (result.widgets.length > 0) {
|
|
840
894
|
console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
|
|
841
895
|
return { text: reply, widgets: result.widgets };
|
|
@@ -908,7 +962,7 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
908
962
|
});
|
|
909
963
|
const summary = result.text.trim();
|
|
910
964
|
let reply = summary || "(the agent produced no reply)";
|
|
911
|
-
if (result.
|
|
965
|
+
if (result.plans?.length) reply = result.plans[0] ?? reply;
|
|
912
966
|
if (installResult.status === "failed") {
|
|
913
967
|
reply += `
|
|
914
968
|
|
|
@@ -937,7 +991,12 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
937
991
|
if (outcome.kind !== "none") {
|
|
938
992
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
939
993
|
}
|
|
940
|
-
return {
|
|
994
|
+
return {
|
|
995
|
+
text: reply,
|
|
996
|
+
widgets: [],
|
|
997
|
+
...outcome.kind === "pr" ? { pr: outcome.pr } : {},
|
|
998
|
+
...result.plans?.length ? { plans: result.plans } : {}
|
|
999
|
+
};
|
|
941
1000
|
}
|
|
942
1001
|
async function heartbeat(config) {
|
|
943
1002
|
const health = await checkClaudeCode();
|
|
@@ -976,8 +1035,14 @@ async function pollLoop(config) {
|
|
|
976
1035
|
continue;
|
|
977
1036
|
}
|
|
978
1037
|
try {
|
|
979
|
-
const { text, widgets, pr } = await processJob(ctx);
|
|
980
|
-
await reportJob(config, ctx.jobId, {
|
|
1038
|
+
const { text, widgets, pr, plans } = await processJob(ctx);
|
|
1039
|
+
await reportJob(config, ctx.jobId, {
|
|
1040
|
+
status: "done",
|
|
1041
|
+
text,
|
|
1042
|
+
widgets,
|
|
1043
|
+
pr,
|
|
1044
|
+
...plans?.length ? { plans } : {}
|
|
1045
|
+
});
|
|
981
1046
|
console.log(`\u2713 Job ${ctx.jobId} done`);
|
|
982
1047
|
} catch (err) {
|
|
983
1048
|
const message = errorMessage2(err);
|
|
@@ -1042,14 +1107,18 @@ function parseFlags(args) {
|
|
|
1042
1107
|
}
|
|
1043
1108
|
var command = process.argv[2];
|
|
1044
1109
|
var rest = process.argv.slice(3);
|
|
1045
|
-
if (command === "
|
|
1110
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
1111
|
+
console.log(RUNNER_VERSION);
|
|
1112
|
+
process.exit(0);
|
|
1113
|
+
} else if (command === "login") {
|
|
1046
1114
|
void login(rest);
|
|
1047
1115
|
} else if (command === "start") {
|
|
1048
1116
|
void start();
|
|
1049
1117
|
} else {
|
|
1050
|
-
console.log(
|
|
1118
|
+
console.log(`FlumeCode runner v${RUNNER_VERSION}`);
|
|
1051
1119
|
console.log("Usage:");
|
|
1052
|
-
console.log(" flumecode login
|
|
1053
|
-
console.log(" flumecode start
|
|
1120
|
+
console.log(" flumecode login # save server URL + token");
|
|
1121
|
+
console.log(" flumecode start # poll for and run jobs");
|
|
1122
|
+
console.log(" flumecode --version # print the runner version");
|
|
1054
1123
|
process.exit(command ? 1 : 0);
|
|
1055
1124
|
}
|
package/package.json
CHANGED
|
@@ -84,13 +84,13 @@ plan without re-deriving it.
|
|
|
84
84
|
|
|
85
85
|
### Multiple plans per request
|
|
86
86
|
|
|
87
|
-
A single request can yield **several** plans — one thread can be accepted into
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
commenting to refine it; treat a later turn as a fresh **Plan** phase and call
|
|
93
|
-
`submit_plan` again with the revised fields.
|
|
87
|
+
A single request can yield **several** plans — one thread can be accepted into many. If the
|
|
88
|
+
work naturally splits into independent pieces, or the user asks for more than one plan, make
|
|
89
|
+
**ONE `submit_plan` call** with all of them in the `plans[]` array (one entry per plan, each
|
|
90
|
+
with a distinct title). Do **not** call `submit_plan` more than once. Each entry becomes its
|
|
91
|
+
own independently-acceptable "Accept as plan" draft. After a plan is accepted the user may
|
|
92
|
+
keep commenting to refine it; treat a later turn as a fresh **Plan** phase and call
|
|
93
|
+
`submit_plan` again with a `plans[]` array containing the revised fields.
|
|
94
94
|
|
|
95
95
|
## Always
|
|
96
96
|
|
|
@@ -39,11 +39,11 @@ actual code. Pick exactly one:
|
|
|
39
39
|
Explain why in plain prose, offer an alternative if you have one, and end your
|
|
40
40
|
turn. Make no code changes.
|
|
41
41
|
- **Re-plan** — the request meaningfully changes scope or direction, enough that a
|
|
42
|
-
fresh plan should be agreed before building. Call **`submit_plan`** with
|
|
43
|
-
revised structured fields (same shape as the request-to-plan skill:
|
|
44
|
-
`goal`, `assumptions`, `steps`, `acceptanceCriteria` — at least 2 —, `risks`,
|
|
45
|
-
`outOfScope`). The runner posts it as a revision
|
|
46
|
-
code changes this turn.
|
|
42
|
+
fresh plan should be agreed before building. Call **`submit_plan`** with a `plans[]` array
|
|
43
|
+
containing the revised structured fields (same per-plan shape as the request-to-plan skill:
|
|
44
|
+
`scope`, `goal`, `assumptions`, `steps`, `acceptanceCriteria` — at least 2 —, `risks`,
|
|
45
|
+
`outOfScope`). Include only one entry for a revise turn. The runner posts it as a revision
|
|
46
|
+
the user can accept; make no code changes this turn.
|
|
47
47
|
- **Implement** — the request is clear and reasonable. Make the change (via
|
|
48
48
|
subagents — see Step 2). This is the common case for small fine-tuning.
|
|
49
49
|
|