@flumecode/runner 0.0.1 → 0.2.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
@@ -175,6 +175,9 @@ var stepSchema = z2.object({
175
175
  files: z2.array(z2.string()).optional().describe("Affected file paths.")
176
176
  });
177
177
  var planInputSchema = {
178
+ title: z2.string().min(1).max(120).describe(
179
+ "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."
180
+ ),
178
181
  scope: z2.enum(["feat", "fix", "chore", "docs", "test", "refactor"]).describe("The primary intent of the change."),
179
182
  goal: z2.string().min(1).describe("One or two sentences stating the outcome."),
180
183
  assumptions: z2.array(z2.string()).describe("Anything decided during planning, including unanswered defaults."),
@@ -188,6 +191,8 @@ var planInputSchema = {
188
191
  var planSchema = z2.object(planInputSchema);
189
192
  function renderPlan(plan) {
190
193
  const lines = [];
194
+ lines.push(`# ${plan.title}`);
195
+ lines.push("");
191
196
  lines.push(`**Scope** \u2014 \`${plan.scope}\``);
192
197
  lines.push("");
193
198
  lines.push(`**Goal** \u2014 ${plan.goal}`);
@@ -235,7 +240,7 @@ function createPlanTooling() {
235
240
  let renderedPlan = null;
236
241
  const submitPlan = tool2(
237
242
  SUBMIT_PLAN,
238
- "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.",
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.",
239
244
  planInputSchema,
240
245
  async (args) => {
241
246
  renderedPlan = renderPlan(planSchema.parse(args));
@@ -400,6 +405,41 @@ function buildPrompt(ctx) {
400
405
  );
401
406
  return lines.join("\n");
402
407
  }
408
+ function buildRevisePrompt(ctx) {
409
+ const task = `Use the \`flumecode:revise-implementation\` skill to handle this turn. The plan below was already implemented (its report is included); 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.`;
410
+ 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.`;
411
+ 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.`;
412
+ const lines = [
413
+ `You are "${ctx.agentName}", an autonomous coding agent fine-tuning an implemented FlumeCode plan in an ongoing thread with the user.`,
414
+ `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.`,
415
+ task,
416
+ orient,
417
+ widgets,
418
+ "",
419
+ "These coding guidelines apply to all code produced in this run:",
420
+ "",
421
+ loadRule("coding-guideline"),
422
+ "",
423
+ `# Plan: ${ctx.request?.title ?? ""}`
424
+ ];
425
+ if (ctx.request?.body) {
426
+ lines.push("", ctx.request.body);
427
+ }
428
+ if (ctx.priorReport) {
429
+ lines.push("", "# Latest implementation report", "", ctx.priorReport);
430
+ }
431
+ if (ctx.thread && ctx.thread.length > 0) {
432
+ lines.push("", "# Conversation so far");
433
+ for (const turn of ctx.thread) {
434
+ lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
435
+ }
436
+ }
437
+ lines.push(
438
+ "",
439
+ "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."
440
+ );
441
+ return lines.join("\n");
442
+ }
403
443
  function buildDocumentPrompt(ctx) {
404
444
  const lines = [
405
445
  `You are "${ctx.agentName}" maintaining the repository wiki for ${ctx.repo.fullName}.`,
@@ -496,12 +536,23 @@ async function cloneAtSha(ctx, dir) {
496
536
  await git(["clone", "--quiet", cloneUrl(ctx), dir]);
497
537
  await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
498
538
  }
539
+ async function cloneResumingBranch(ctx, dir) {
540
+ await git(["clone", "--quiet", cloneUrl(ctx), dir]);
541
+ try {
542
+ await git(["-C", dir, "fetch", "--quiet", "origin", ctx.repo.checkoutBranch]);
543
+ await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, "FETCH_HEAD"]);
544
+ return { resumed: true };
545
+ } catch {
546
+ await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
547
+ return { resumed: false };
548
+ }
549
+ }
499
550
  async function hasChanges(dir) {
500
551
  await git(["-C", dir, "add", "-A"]);
501
552
  const { stdout: stdout2 } = await git(["-C", dir, "status", "--porcelain"]);
502
553
  return stdout2.trim().length > 0;
503
554
  }
504
- async function commitAndPush(ctx, dir) {
555
+ async function commitChanges(ctx, dir) {
505
556
  if (!await hasChanges(dir)) return false;
506
557
  await git([
507
558
  "-C",
@@ -515,9 +566,36 @@ async function commitAndPush(ctx, dir) {
515
566
  "-m",
516
567
  `FlumeCode: ${jobTitle(ctx)}`
517
568
  ]);
518
- await git(["-C", dir, "push", "--quiet", "-u", "origin", ctx.repo.checkoutBranch]);
519
569
  return true;
520
570
  }
571
+ async function pushBranch(ctx, dir) {
572
+ await git(["-C", dir, "push", "--quiet", "-u", "origin", ctx.repo.checkoutBranch]);
573
+ }
574
+ var RebaseConflictError = class extends Error {
575
+ constructor(mergeBranch, files) {
576
+ const list = files.length ? `: ${files.join(", ")}` : "";
577
+ super(`Rebase onto ${mergeBranch} hit conflicts in ${files.length} file(s)${list}`);
578
+ this.mergeBranch = mergeBranch;
579
+ this.files = files;
580
+ this.name = "RebaseConflictError";
581
+ }
582
+ };
583
+ async function rebaseOntoMergeBranch(ctx, dir) {
584
+ const { mergeBranch } = ctx.repo;
585
+ if (!mergeBranch) return;
586
+ await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
587
+ try {
588
+ await git(["-C", dir, "rebase", "FETCH_HEAD"]);
589
+ } catch {
590
+ const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
591
+ () => ({ stdout: "" })
592
+ );
593
+ const files = conflicted.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
594
+ await git(["-C", dir, "rebase", "--abort"]).catch(() => {
595
+ });
596
+ throw new RebaseConflictError(mergeBranch, files);
597
+ }
598
+ }
521
599
  async function openPullRequest(ctx) {
522
600
  const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
523
601
  if (!mergeBranch) return null;
@@ -568,9 +646,11 @@ var ORCHESTRATOR_MAX_TURNS = 80;
568
646
  var INIT_MAX_TURNS = 200;
569
647
  var DOCUMENT_MAX_TURNS = 120;
570
648
  var HEARTBEAT_MS = 5 * 6e4;
571
- async function pushAndOpenPr(ctx, dir) {
572
- const pushed = await commitAndPush(ctx, dir);
573
- if (!pushed) return { kind: "none" };
649
+ async function pushAndOpenPr(ctx, dir, opts = { rebase: true }) {
650
+ const committed = await commitChanges(ctx, dir);
651
+ if (!committed) return { kind: "none" };
652
+ if (opts.rebase) await rebaseOntoMergeBranch(ctx, dir);
653
+ await pushBranch(ctx, dir);
574
654
  const pr = await openPullRequest(ctx);
575
655
  return pr ? { kind: "pr", pr } : { kind: "pushed" };
576
656
  }
@@ -599,6 +679,7 @@ async function processJob(ctx) {
599
679
  try {
600
680
  if (ctx.kind === "init") return await processInitJob(ctx, dir);
601
681
  if (ctx.kind === "implement") return await processImplementJob(ctx, dir);
682
+ if (ctx.kind === "revise") return await processReviseJob(ctx, dir);
602
683
  return await processChatJob(ctx, dir);
603
684
  } finally {
604
685
  await cleanup(dir);
@@ -670,7 +751,7 @@ async function processChatJob(ctx, dir) {
670
751
  async function processImplementJob(ctx, dir) {
671
752
  console.log(`
672
753
  \u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
673
- await cloneAtSha(ctx, dir);
754
+ const { resumed } = await cloneResumingBranch(ctx, dir);
674
755
  const installResult = await installDependencies(dir);
675
756
  const result = await runClaudeCode({
676
757
  cwd: dir,
@@ -700,10 +781,55 @@ async function processImplementJob(ctx, dir) {
700
781
  console.warn(` wiki update skipped: ${errorMessage2(err)}`);
701
782
  }
702
783
  }
703
- const outcome = await pushAndOpenPr(ctx, dir);
784
+ const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
704
785
  reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
705
786
  return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
706
787
  }
788
+ async function processReviseJob(ctx, dir) {
789
+ console.log(`
790
+ \u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
791
+ const { resumed } = await cloneResumingBranch(ctx, dir);
792
+ const installResult = await installDependencies(dir);
793
+ const result = await runClaudeCode({
794
+ cwd: dir,
795
+ prompt: buildRevisePrompt(ctx),
796
+ permissionMode: ctx.permissionMode,
797
+ model: ORCHESTRATOR_MODEL,
798
+ maxTurns: ORCHESTRATOR_MAX_TURNS
799
+ });
800
+ const summary = result.text.trim();
801
+ let reply = summary || "(the agent produced no reply)";
802
+ if (result.plan) reply = result.plan;
803
+ if (installResult.status === "failed") {
804
+ reply += `
805
+
806
+ > \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
807
+ }
808
+ if (result.widgets.length > 0) {
809
+ console.log(` \u2026revise ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
810
+ return { text: reply, widgets: result.widgets };
811
+ }
812
+ let documented = false;
813
+ if (await hasChanges(dir)) {
814
+ try {
815
+ console.log(` \u2026updating wiki for revise ${ctx.jobId}`);
816
+ await runClaudeCode({
817
+ cwd: dir,
818
+ prompt: buildDocumentPrompt(ctx),
819
+ permissionMode: ctx.permissionMode,
820
+ maxTurns: DOCUMENT_MAX_TURNS
821
+ });
822
+ documented = true;
823
+ } catch (err) {
824
+ console.warn(` wiki update skipped: ${errorMessage2(err)}`);
825
+ }
826
+ }
827
+ const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
828
+ if (outcome.kind !== "none") {
829
+ reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
830
+ }
831
+ return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
832
+ }
707
833
  async function heartbeat(config) {
708
834
  const health = await checkClaudeCode();
709
835
  if (health.ready) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flumecode/runner",
3
- "version": "0.0.1",
3
+ "version": "0.2.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": {
@@ -57,6 +57,8 @@ it as your comment — do **not** write the plan as your reply text.
57
57
 
58
58
  Field-by-field guidance:
59
59
 
60
+ - **`title`** — a concise, descriptive name for this specific plan. Must be distinct from the
61
+ request title and from any sibling plans on the same request. Keep it under 120 characters.
60
62
  - **`scope`** — exactly one of `feat`, `fix`, `chore`, `docs`, `test`,
61
63
  `refactor`. Pick the one that best matches the primary intent of the request.
62
64
  - **`goal`** — one or two sentences stating the outcome, phrased so it directly
@@ -85,9 +87,10 @@ plan without re-deriving it.
85
87
  A single request can yield **several** plans — one thread can be accepted into
86
88
  many. If the work naturally splits into independent pieces, or the user asks for
87
89
  more than one plan, call `submit_plan` once for each finished plan so each can
88
- be accepted into its own GitHub issue. After a plan is accepted the user may
89
- keep commenting to refine it; treat a later turn as a fresh **Plan** phase and
90
- call `submit_plan` again with the revised fields.
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.
91
94
 
92
95
  ## Always
93
96
 
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: revise-implementation
3
+ description: >-
4
+ Handle a follow-up turn on an already-implemented plan, where the user asks to
5
+ fine-tune the result. Use in edit-capable plan-thread runs. First decide how to
6
+ respond — clarify, push back, propose a revised plan, or implement the change —
7
+ then act: for code changes you are the orchestrator (delegate to subagents like
8
+ implement-plan) and the runner updates the existing pull request. Never commits,
9
+ pushes, or opens a PR yourself.
10
+ ---
11
+
12
+ # revise-implementation
13
+
14
+ The plan in the context above was **already implemented** — its latest report and
15
+ the open pull request exist, and your working directory is checked out on that
16
+ PR's branch. The user has now posted a follow-up message asking to fine-tune the
17
+ result. Your job is to respond to that message well: sometimes by changing code,
18
+ but just as often by asking, pushing back, or re-planning.
19
+
20
+ ## You are stateless — orient yourself first
21
+
22
+ Each run you see the whole thread but keep **no memory** between turns. You cannot
23
+ pause mid-run to wait for the user: to ask something, end your turn with the
24
+ question and the user's reply starts a fresh run that re-enters this skill. So
25
+ read the conversation first — the **last user message is this turn's request** —
26
+ and note anything you already asked and they already answered (treat it settled).
27
+
28
+ ## Step 1 — Decide how to respond
29
+
30
+ Read the latest user message against the plan, the implementation report, and the
31
+ actual code. Pick exactly one:
32
+
33
+ - **Clarify** — the request is ambiguous or under-specified. Ask the user, then
34
+ stop. When it's a clean choice, ask it as a widget: `single_select` for one-of-N,
35
+ `multi_select` for "select all that apply" (don't add your own "Other" — the UI
36
+ always offers one). Otherwise ask in prose. After asking, **end your turn**; make
37
+ no code changes.
38
+ - **Push back** — the request is a bad idea, unsafe, or not feasible as asked.
39
+ Explain why in plain prose, offer an alternative if you have one, and end your
40
+ turn. Make no code changes.
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.
47
+ - **Implement** — the request is clear and reasonable. Make the change (via
48
+ subagents — see Step 2). This is the common case for small fine-tuning.
49
+
50
+ When in doubt between Clarify and Implement, prefer a quick clarifying question
51
+ over guessing on anything that would be costly to redo.
52
+
53
+ ## Step 2 — Implement (only if you chose Implement)
54
+
55
+ You are the **orchestrator**, exactly as in the `implement-plan` skill: do not
56
+ write the code yourself — delegate each phase to **Task** subagents and pick the
57
+ right model per phase. Read `implement-plan` if you need the full pipeline; the
58
+ essentials:
59
+
60
+ - **Subagents start blank.** Each Task subagent sees only the prompt you give it —
61
+ not this thread, the plan, or the prior report. Make every prompt self-contained:
62
+ include the specific change requested, the relevant plan/report excerpt, the code
63
+ context, and the coding guidelines (verbatim, from the `# Coding Guidelines`
64
+ section in the prompt).
65
+ - **Scope the work to the request.** This is a fine-tune of an existing
66
+ implementation, not a rebuild. Change only what the user asked for plus what that
67
+ change strictly requires; don't regress the rest of the plan.
68
+ - **Pipeline:** Implement (Task, `model: "sonnet"`) → acceptance/quality review of
69
+ the change (Task, `model: "opus"`, read-only) → fix loop if needed (≤2) → report
70
+ (Task, `model: "opus"`, read-only). Reviewers and the report writer never edit.
71
+ - **No git side effects.** Never commit, push, or open a PR — leave the changes in
72
+ the working tree. The runner commits them and updates the existing pull request.
73
+
74
+ ## Your final reply
75
+
76
+ Your last message **is** the comment posted to the plan thread — write it for the
77
+ user:
78
+
79
+ - **Implemented:** a short report — what you changed and why, which files, and how
80
+ it was verified (build/tests). The runner appends the pull-request link, so don't
81
+ add one.
82
+ - **Clarify / push back:** your question or reasoning, as prose (plus any widget).
83
+ - **Re-plan:** you called `submit_plan`; the rendered plan is posted automatically,
84
+ so keep any extra reply text minimal.
85
+
86
+ ## Always
87
+
88
+ - Decide before you act; don't implement an ambiguous or ill-advised request.
89
+ - Delegate code changes through Task subagents — don't write code yourself.
90
+ - Keep the change scoped to this turn's request; don't regress the implementation.
91
+ - Never commit, push, or open a PR.
92
+ - Your final message is what the user reads.