@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 renderedPlan = null;
293
+ let renderedPlans = null;
241
294
  const submitPlan = tool2(
242
295
  SUBMIT_PLAN,
243
- "Submit the finished plan. Call this \u2014 and only this \u2014 when the plan is complete and ready to post. The runner renders your structured fields into the canonical plan markdown and posts it as your comment. acceptanceCriteria is required and must contain at least 2 observable, verifiable conditions (behaviors, tests, or states you could check) that together define 'done'. After calling this, end your turn. The `title` field names this specific plan \u2014 make it concise and distinct from the request title.",
244
- planInputSchema,
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
- renderedPlan = renderPlan(planSchema.parse(args));
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 it as your comment. End your turn now."
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, getPlan: () => renderedPlan };
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, getPlan } = createPlanTooling();
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, plan: getPlan() };
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.plan) reply = result.plan;
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 { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
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, { status: "done", text, widgets, pr });
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 === "login") {
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("FlumeCode runner");
1118
+ console.log(`FlumeCode runner v${RUNNER_VERSION}`);
1051
1119
  console.log("Usage:");
1052
- console.log(" flumecode login # save server URL + token");
1053
- console.log(" flumecode start # poll for and run jobs");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flumecode/runner",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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": {
@@ -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
- many. If the work naturally splits into independent pieces, or the user asks for
89
- more than one plan, call `submit_plan` once for each finished plan so each can
90
- be accepted into its own GitHub issue. Give each plan its own distinct `title`
91
- so sibling plans don't collide. After a plan is accepted the user may keep
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 the
43
- revised structured fields (same shape as the request-to-plan skill: `scope`,
44
- `goal`, `assumptions`, `steps`, `acceptanceCriteria` — at least 2 —, `risks`,
45
- `outOfScope`). The runner posts it as a revision the user can accept; make no
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