@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
|
|
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
|
|
573
|
-
if (!
|
|
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
|
|
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
|
@@ -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.
|
|
89
|
-
|
|
90
|
-
|
|
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.
|