@flumecode/runner 0.0.1 → 0.1.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
|
@@ -400,6 +400,41 @@ function buildPrompt(ctx) {
|
|
|
400
400
|
);
|
|
401
401
|
return lines.join("\n");
|
|
402
402
|
}
|
|
403
|
+
function buildRevisePrompt(ctx) {
|
|
404
|
+
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.`;
|
|
405
|
+
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.`;
|
|
406
|
+
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.`;
|
|
407
|
+
const lines = [
|
|
408
|
+
`You are "${ctx.agentName}", an autonomous coding agent fine-tuning an implemented FlumeCode plan in an ongoing thread with the user.`,
|
|
409
|
+
`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.`,
|
|
410
|
+
task,
|
|
411
|
+
orient,
|
|
412
|
+
widgets,
|
|
413
|
+
"",
|
|
414
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
415
|
+
"",
|
|
416
|
+
loadRule("coding-guideline"),
|
|
417
|
+
"",
|
|
418
|
+
`# Plan: ${ctx.request?.title ?? ""}`
|
|
419
|
+
];
|
|
420
|
+
if (ctx.request?.body) {
|
|
421
|
+
lines.push("", ctx.request.body);
|
|
422
|
+
}
|
|
423
|
+
if (ctx.priorReport) {
|
|
424
|
+
lines.push("", "# Latest implementation report", "", ctx.priorReport);
|
|
425
|
+
}
|
|
426
|
+
if (ctx.thread && ctx.thread.length > 0) {
|
|
427
|
+
lines.push("", "# Conversation so far");
|
|
428
|
+
for (const turn of ctx.thread) {
|
|
429
|
+
lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
lines.push(
|
|
433
|
+
"",
|
|
434
|
+
"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."
|
|
435
|
+
);
|
|
436
|
+
return lines.join("\n");
|
|
437
|
+
}
|
|
403
438
|
function buildDocumentPrompt(ctx) {
|
|
404
439
|
const lines = [
|
|
405
440
|
`You are "${ctx.agentName}" maintaining the repository wiki for ${ctx.repo.fullName}.`,
|
|
@@ -496,12 +531,23 @@ async function cloneAtSha(ctx, dir) {
|
|
|
496
531
|
await git(["clone", "--quiet", cloneUrl(ctx), dir]);
|
|
497
532
|
await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
|
|
498
533
|
}
|
|
534
|
+
async function cloneResumingBranch(ctx, dir) {
|
|
535
|
+
await git(["clone", "--quiet", cloneUrl(ctx), dir]);
|
|
536
|
+
try {
|
|
537
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", ctx.repo.checkoutBranch]);
|
|
538
|
+
await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, "FETCH_HEAD"]);
|
|
539
|
+
return { resumed: true };
|
|
540
|
+
} catch {
|
|
541
|
+
await git(["-C", dir, "checkout", "-B", ctx.repo.checkoutBranch, ctx.repo.checkoutSha]);
|
|
542
|
+
return { resumed: false };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
499
545
|
async function hasChanges(dir) {
|
|
500
546
|
await git(["-C", dir, "add", "-A"]);
|
|
501
547
|
const { stdout: stdout2 } = await git(["-C", dir, "status", "--porcelain"]);
|
|
502
548
|
return stdout2.trim().length > 0;
|
|
503
549
|
}
|
|
504
|
-
async function
|
|
550
|
+
async function commitChanges(ctx, dir) {
|
|
505
551
|
if (!await hasChanges(dir)) return false;
|
|
506
552
|
await git([
|
|
507
553
|
"-C",
|
|
@@ -515,9 +561,36 @@ async function commitAndPush(ctx, dir) {
|
|
|
515
561
|
"-m",
|
|
516
562
|
`FlumeCode: ${jobTitle(ctx)}`
|
|
517
563
|
]);
|
|
518
|
-
await git(["-C", dir, "push", "--quiet", "-u", "origin", ctx.repo.checkoutBranch]);
|
|
519
564
|
return true;
|
|
520
565
|
}
|
|
566
|
+
async function pushBranch(ctx, dir) {
|
|
567
|
+
await git(["-C", dir, "push", "--quiet", "-u", "origin", ctx.repo.checkoutBranch]);
|
|
568
|
+
}
|
|
569
|
+
var RebaseConflictError = class extends Error {
|
|
570
|
+
constructor(mergeBranch, files) {
|
|
571
|
+
const list = files.length ? `: ${files.join(", ")}` : "";
|
|
572
|
+
super(`Rebase onto ${mergeBranch} hit conflicts in ${files.length} file(s)${list}`);
|
|
573
|
+
this.mergeBranch = mergeBranch;
|
|
574
|
+
this.files = files;
|
|
575
|
+
this.name = "RebaseConflictError";
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
async function rebaseOntoMergeBranch(ctx, dir) {
|
|
579
|
+
const { mergeBranch } = ctx.repo;
|
|
580
|
+
if (!mergeBranch) return;
|
|
581
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
|
|
582
|
+
try {
|
|
583
|
+
await git(["-C", dir, "rebase", "FETCH_HEAD"]);
|
|
584
|
+
} catch {
|
|
585
|
+
const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
|
|
586
|
+
() => ({ stdout: "" })
|
|
587
|
+
);
|
|
588
|
+
const files = conflicted.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
589
|
+
await git(["-C", dir, "rebase", "--abort"]).catch(() => {
|
|
590
|
+
});
|
|
591
|
+
throw new RebaseConflictError(mergeBranch, files);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
521
594
|
async function openPullRequest(ctx) {
|
|
522
595
|
const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
|
|
523
596
|
if (!mergeBranch) return null;
|
|
@@ -568,9 +641,11 @@ var ORCHESTRATOR_MAX_TURNS = 80;
|
|
|
568
641
|
var INIT_MAX_TURNS = 200;
|
|
569
642
|
var DOCUMENT_MAX_TURNS = 120;
|
|
570
643
|
var HEARTBEAT_MS = 5 * 6e4;
|
|
571
|
-
async function pushAndOpenPr(ctx, dir) {
|
|
572
|
-
const
|
|
573
|
-
if (!
|
|
644
|
+
async function pushAndOpenPr(ctx, dir, opts = { rebase: true }) {
|
|
645
|
+
const committed = await commitChanges(ctx, dir);
|
|
646
|
+
if (!committed) return { kind: "none" };
|
|
647
|
+
if (opts.rebase) await rebaseOntoMergeBranch(ctx, dir);
|
|
648
|
+
await pushBranch(ctx, dir);
|
|
574
649
|
const pr = await openPullRequest(ctx);
|
|
575
650
|
return pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
576
651
|
}
|
|
@@ -599,6 +674,7 @@ async function processJob(ctx) {
|
|
|
599
674
|
try {
|
|
600
675
|
if (ctx.kind === "init") return await processInitJob(ctx, dir);
|
|
601
676
|
if (ctx.kind === "implement") return await processImplementJob(ctx, dir);
|
|
677
|
+
if (ctx.kind === "revise") return await processReviseJob(ctx, dir);
|
|
602
678
|
return await processChatJob(ctx, dir);
|
|
603
679
|
} finally {
|
|
604
680
|
await cleanup(dir);
|
|
@@ -670,7 +746,7 @@ async function processChatJob(ctx, dir) {
|
|
|
670
746
|
async function processImplementJob(ctx, dir) {
|
|
671
747
|
console.log(`
|
|
672
748
|
\u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
673
|
-
await
|
|
749
|
+
const { resumed } = await cloneResumingBranch(ctx, dir);
|
|
674
750
|
const installResult = await installDependencies(dir);
|
|
675
751
|
const result = await runClaudeCode({
|
|
676
752
|
cwd: dir,
|
|
@@ -700,10 +776,55 @@ async function processImplementJob(ctx, dir) {
|
|
|
700
776
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
701
777
|
}
|
|
702
778
|
}
|
|
703
|
-
const outcome = await pushAndOpenPr(ctx, dir);
|
|
779
|
+
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
704
780
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
705
781
|
return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
|
|
706
782
|
}
|
|
783
|
+
async function processReviseJob(ctx, dir) {
|
|
784
|
+
console.log(`
|
|
785
|
+
\u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
786
|
+
const { resumed } = await cloneResumingBranch(ctx, dir);
|
|
787
|
+
const installResult = await installDependencies(dir);
|
|
788
|
+
const result = await runClaudeCode({
|
|
789
|
+
cwd: dir,
|
|
790
|
+
prompt: buildRevisePrompt(ctx),
|
|
791
|
+
permissionMode: ctx.permissionMode,
|
|
792
|
+
model: ORCHESTRATOR_MODEL,
|
|
793
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
794
|
+
});
|
|
795
|
+
const summary = result.text.trim();
|
|
796
|
+
let reply = summary || "(the agent produced no reply)";
|
|
797
|
+
if (result.plan) reply = result.plan;
|
|
798
|
+
if (installResult.status === "failed") {
|
|
799
|
+
reply += `
|
|
800
|
+
|
|
801
|
+
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
802
|
+
}
|
|
803
|
+
if (result.widgets.length > 0) {
|
|
804
|
+
console.log(` \u2026revise ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
|
|
805
|
+
return { text: reply, widgets: result.widgets };
|
|
806
|
+
}
|
|
807
|
+
let documented = false;
|
|
808
|
+
if (await hasChanges(dir)) {
|
|
809
|
+
try {
|
|
810
|
+
console.log(` \u2026updating wiki for revise ${ctx.jobId}`);
|
|
811
|
+
await runClaudeCode({
|
|
812
|
+
cwd: dir,
|
|
813
|
+
prompt: buildDocumentPrompt(ctx),
|
|
814
|
+
permissionMode: ctx.permissionMode,
|
|
815
|
+
maxTurns: DOCUMENT_MAX_TURNS
|
|
816
|
+
});
|
|
817
|
+
documented = true;
|
|
818
|
+
} catch (err) {
|
|
819
|
+
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
823
|
+
if (outcome.kind !== "none") {
|
|
824
|
+
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
825
|
+
}
|
|
826
|
+
return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
|
|
827
|
+
}
|
|
707
828
|
async function heartbeat(config) {
|
|
708
829
|
const health = await checkClaudeCode();
|
|
709
830
|
if (health.ready) {
|
package/package.json
CHANGED
|
@@ -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.
|