@flumecode/runner 0.3.1 → 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
@@ -40,6 +40,7 @@ function readVersion() {
40
40
  var RUNNER_VERSION = readVersion();
41
41
  var RUNNER_VERSION_HEADER = "x-flumecode-runner-version";
42
42
  var RUNNER_MIN_VERSION_HEADER = "x-flumecode-min-runner-version";
43
+ var RUNNER_LATEST_VERSION_HEADER = "x-flumecode-latest-runner-version";
43
44
  function compareVersions(a, b) {
44
45
  const parse = (v) => (v.split("-")[0] ?? v).split(".").map((n) => Number.parseInt(n, 10) || 0);
45
46
  const pa = parse(a);
@@ -55,16 +56,29 @@ function compareVersions(a, b) {
55
56
  // src/api.ts
56
57
  var OUTDATED_WARN_INTERVAL_MS = 60 * 6e4;
57
58
  var lastOutdatedWarnAt = 0;
59
+ var UPDATE_NUDGE_INTERVAL_MS = 60 * 6e4;
60
+ var lastUpdateNudgeAt = 0;
58
61
  function noteServerVersion(res) {
59
62
  const min = res.headers.get(RUNNER_MIN_VERSION_HEADER)?.trim();
60
- if (!min || RUNNER_VERSION === "unknown") return;
61
- if (compareVersions(RUNNER_VERSION, min) >= 0) return;
62
- const now = Date.now();
63
- if (now - lastOutdatedWarnAt < OUTDATED_WARN_INTERVAL_MS) return;
64
- lastOutdatedWarnAt = now;
65
- console.warn(
66
- `\u26A0\uFE0F This runner (v${RUNNER_VERSION}) is outdated \u2014 the server expects v${min} or newer.
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.
67
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`
68
82
  );
69
83
  }
70
84
  async function claimJob(config) {
@@ -265,19 +279,30 @@ function renderPlan(plan) {
265
279
  lines.push(PLAN_MARKER);
266
280
  return lines.join("\n");
267
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);
268
292
  function createPlanTooling() {
269
- let renderedPlan = null;
293
+ let renderedPlans = null;
270
294
  const submitPlan = tool2(
271
295
  SUBMIT_PLAN,
272
- "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.",
273
- 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,
274
298
  async (args) => {
275
- renderedPlan = renderPlan(planSchema.parse(args));
299
+ const parsed = submitPlanSchema.parse(args);
300
+ renderedPlans = parsed.plans.map(renderPlan);
276
301
  return {
277
302
  content: [
278
303
  {
279
304
  type: "text",
280
- 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."
281
306
  }
282
307
  ]
283
308
  };
@@ -287,7 +312,7 @@ function createPlanTooling() {
287
312
  name: SERVER_NAME2,
288
313
  tools: [submitPlan]
289
314
  });
290
- return { mcpServer, getPlan: () => renderedPlan };
315
+ return { mcpServer, getPlans: () => renderedPlans };
291
316
  }
292
317
 
293
318
  // src/executor.ts
@@ -295,7 +320,7 @@ var FLUME_PLUGIN_DIR = fileURLToPath2(new URL("../skills-plugin", import.meta.ur
295
320
  async function runClaudeCode(opts) {
296
321
  let finalText = "";
297
322
  const { mcpServer, collected } = createWidgetTooling();
298
- const { mcpServer: planServer, getPlan } = createPlanTooling();
323
+ const { mcpServer: planServer, getPlans } = createPlanTooling();
299
324
  for await (const message of query({
300
325
  prompt: opts.prompt,
301
326
  options: {
@@ -333,7 +358,7 @@ async function runClaudeCode(opts) {
333
358
  }
334
359
  }
335
360
  process.stdout.write("\n");
336
- return { text: finalText, widgets: collected, plan: getPlan() };
361
+ return { text: finalText, widgets: collected, plans: getPlans() };
337
362
  }
338
363
 
339
364
  // src/health.ts
@@ -857,14 +882,14 @@ async function processChatJob(ctx, dir) {
857
882
  });
858
883
  const summary = result.text.trim();
859
884
  let reply = summary || "(the agent produced no summary)";
860
- if (result.plan) {
861
- reply = result.plan;
862
- }
863
885
  if (installResult?.status === "failed") {
864
886
  reply += `
865
887
 
866
888
  > \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
867
889
  }
890
+ if (result.plans?.length) {
891
+ return { text: result.text.trim(), widgets: [], plans: result.plans };
892
+ }
868
893
  if (result.widgets.length > 0) {
869
894
  console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
870
895
  return { text: reply, widgets: result.widgets };
@@ -937,7 +962,7 @@ async function processReviseJob(ctx, dir, resumed) {
937
962
  });
938
963
  const summary = result.text.trim();
939
964
  let reply = summary || "(the agent produced no reply)";
940
- if (result.plan) reply = result.plan;
965
+ if (result.plans?.length) reply = result.plans[0] ?? reply;
941
966
  if (installResult.status === "failed") {
942
967
  reply += `
943
968
 
@@ -966,7 +991,12 @@ async function processReviseJob(ctx, dir, resumed) {
966
991
  if (outcome.kind !== "none") {
967
992
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
968
993
  }
969
- 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
+ };
970
1000
  }
971
1001
  async function heartbeat(config) {
972
1002
  const health = await checkClaudeCode();
@@ -1005,8 +1035,14 @@ async function pollLoop(config) {
1005
1035
  continue;
1006
1036
  }
1007
1037
  try {
1008
- const { text, widgets, pr } = await processJob(ctx);
1009
- 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
+ });
1010
1046
  console.log(`\u2713 Job ${ctx.jobId} done`);
1011
1047
  } catch (err) {
1012
1048
  const message = errorMessage2(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flumecode/runner",
3
- "version": "0.3.1",
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