@flumecode/runner 0.1.0 → 0.3.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/README.md +2 -1
- package/dist/cli.js +140 -26
- package/package.json +1 -1
- package/skills-plugin/skills/request-to-plan/SKILL.md +6 -3
package/README.md
CHANGED
|
@@ -45,7 +45,8 @@ pnpm --filter @flumecode/runner dev login # or: dev start
|
|
|
45
45
|
|
|
46
46
|
`pnpm --filter @flumecode/runner build` produces the published single-file bundle
|
|
47
47
|
(`dist/cli.js`); it is published to npm by the `release-runner` GitHub workflow
|
|
48
|
-
when a
|
|
48
|
+
when a GitHub Release is created (publishing the version in `package.json`, and
|
|
49
|
+
skipping if that version is already on npm).
|
|
49
50
|
|
|
50
51
|
## What a job does
|
|
51
52
|
|
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));
|
|
@@ -455,6 +460,25 @@ function buildDocumentPrompt(ctx) {
|
|
|
455
460
|
lines.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
|
|
456
461
|
return lines.join("\n");
|
|
457
462
|
}
|
|
463
|
+
function buildRepairPrompt(ctx, hookLog) {
|
|
464
|
+
const lines = [
|
|
465
|
+
`You are "${ctx.agentName}", fixing a failed pre-commit check in the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
466
|
+
`The changes from the previous step are still uncommitted in the working tree. When the runner tried to commit them, the repository's pre-commit hook \u2014 which runs the project's own checks (lint / typecheck / unit tests) \u2014 failed. Make the working tree pass those checks: fix the failing code or tests at their root. Do NOT delete or skip tests, weaken assertions, or disable the checks to silence the failure. Preserve the intent of the original change; repair only what's broken. Do NOT commit or push \u2014 the runner re-commits once the checks pass.`,
|
|
467
|
+
"",
|
|
468
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
469
|
+
"",
|
|
470
|
+
loadRule("coding-guideline"),
|
|
471
|
+
"",
|
|
472
|
+
"# Pre-commit hook output",
|
|
473
|
+
"",
|
|
474
|
+
"```",
|
|
475
|
+
hookLog,
|
|
476
|
+
"```",
|
|
477
|
+
"",
|
|
478
|
+
"When done, reply with a one-line summary of what you fixed."
|
|
479
|
+
];
|
|
480
|
+
return lines.join("\n");
|
|
481
|
+
}
|
|
458
482
|
function buildInitPrompt(ctx) {
|
|
459
483
|
return [
|
|
460
484
|
`You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
@@ -508,6 +532,48 @@ async function installDependencies(dir) {
|
|
|
508
532
|
async function makeWorkspace() {
|
|
509
533
|
return mkdtemp(join3(tmpdir(), WORKSPACE_PREFIX));
|
|
510
534
|
}
|
|
535
|
+
var MAX_WORKSPACES = 8;
|
|
536
|
+
var workspaceRegistry = /* @__PURE__ */ new Map();
|
|
537
|
+
async function acquireWorkspace(key) {
|
|
538
|
+
const existing = workspaceRegistry.get(key);
|
|
539
|
+
if (existing !== void 0 && existsSync2(existing)) {
|
|
540
|
+
workspaceRegistry.delete(key);
|
|
541
|
+
workspaceRegistry.set(key, existing);
|
|
542
|
+
return { dir: existing, reused: true };
|
|
543
|
+
}
|
|
544
|
+
const dir = await makeWorkspace();
|
|
545
|
+
workspaceRegistry.set(key, dir);
|
|
546
|
+
if (workspaceRegistry.size > MAX_WORKSPACES) {
|
|
547
|
+
const oldest = workspaceRegistry.keys().next().value;
|
|
548
|
+
const oldDir = workspaceRegistry.get(oldest);
|
|
549
|
+
workspaceRegistry.delete(oldest);
|
|
550
|
+
rm(oldDir, { recursive: true, force: true }).catch(() => {
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return { dir, reused: false };
|
|
554
|
+
}
|
|
555
|
+
async function discardWorkspace(key) {
|
|
556
|
+
const dir = workspaceRegistry.get(key);
|
|
557
|
+
workspaceRegistry.delete(key);
|
|
558
|
+
if (dir !== void 0) {
|
|
559
|
+
await cleanup(dir).catch(() => {
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function prepareAtSha(ctx, dir, reused) {
|
|
564
|
+
if (!reused) {
|
|
565
|
+
await cloneAtSha(ctx, dir);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
|
|
569
|
+
}
|
|
570
|
+
async function prepareResumingBranch(ctx, dir, reused) {
|
|
571
|
+
if (!reused) {
|
|
572
|
+
return cloneResumingBranch(ctx, dir);
|
|
573
|
+
}
|
|
574
|
+
await git(["-C", dir, "remote", "set-url", "origin", cloneUrl(ctx)]);
|
|
575
|
+
return { resumed: true };
|
|
576
|
+
}
|
|
511
577
|
async function sweepWorkspaces() {
|
|
512
578
|
const base = tmpdir();
|
|
513
579
|
let entries;
|
|
@@ -547,20 +613,36 @@ async function hasChanges(dir) {
|
|
|
547
613
|
const { stdout: stdout2 } = await git(["-C", dir, "status", "--porcelain"]);
|
|
548
614
|
return stdout2.trim().length > 0;
|
|
549
615
|
}
|
|
616
|
+
var PreCommitError = class extends Error {
|
|
617
|
+
constructor(log) {
|
|
618
|
+
super("pre-commit checks failed");
|
|
619
|
+
this.log = log;
|
|
620
|
+
this.name = "PreCommitError";
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
function commitFailureLog(err) {
|
|
624
|
+
const e = err;
|
|
625
|
+
const parts = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter((s) => s.length > 0);
|
|
626
|
+
return parts.length > 0 ? parts.join("\n") : e.message ?? String(err);
|
|
627
|
+
}
|
|
550
628
|
async function commitChanges(ctx, dir) {
|
|
551
629
|
if (!await hasChanges(dir)) return false;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
630
|
+
try {
|
|
631
|
+
await git([
|
|
632
|
+
"-C",
|
|
633
|
+
dir,
|
|
634
|
+
"-c",
|
|
635
|
+
"user.email=runner@flumecode.local",
|
|
636
|
+
"-c",
|
|
637
|
+
"user.name=FlumeCode Runner",
|
|
638
|
+
"commit",
|
|
639
|
+
"--quiet",
|
|
640
|
+
"-m",
|
|
641
|
+
`FlumeCode: ${jobTitle(ctx)}`
|
|
642
|
+
]);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
throw new PreCommitError(commitFailureLog(err));
|
|
645
|
+
}
|
|
564
646
|
return true;
|
|
565
647
|
}
|
|
566
648
|
async function pushBranch(ctx, dir) {
|
|
@@ -638,17 +720,37 @@ async function cleanup(dir) {
|
|
|
638
720
|
var IDLE_MS = 5e3;
|
|
639
721
|
var ORCHESTRATOR_MODEL = "sonnet";
|
|
640
722
|
var ORCHESTRATOR_MAX_TURNS = 80;
|
|
723
|
+
var MAX_COMMIT_REPAIRS = 2;
|
|
641
724
|
var INIT_MAX_TURNS = 200;
|
|
642
725
|
var DOCUMENT_MAX_TURNS = 120;
|
|
643
726
|
var HEARTBEAT_MS = 5 * 6e4;
|
|
644
727
|
async function pushAndOpenPr(ctx, dir, opts = { rebase: true }) {
|
|
645
|
-
const committed = await
|
|
728
|
+
const committed = await commitWithRepair(ctx, dir);
|
|
646
729
|
if (!committed) return { kind: "none" };
|
|
647
730
|
if (opts.rebase) await rebaseOntoMergeBranch(ctx, dir);
|
|
648
731
|
await pushBranch(ctx, dir);
|
|
649
732
|
const pr = await openPullRequest(ctx);
|
|
650
733
|
return pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
651
734
|
}
|
|
735
|
+
async function commitWithRepair(ctx, dir) {
|
|
736
|
+
for (let attempt = 1; ; attempt++) {
|
|
737
|
+
try {
|
|
738
|
+
return await commitChanges(ctx, dir);
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (!(err instanceof PreCommitError) || attempt > MAX_COMMIT_REPAIRS) throw err;
|
|
741
|
+
console.warn(
|
|
742
|
+
` pre-commit checks failed (repair ${attempt}/${MAX_COMMIT_REPAIRS}) \u2014 asking the agent to fix\u2026`
|
|
743
|
+
);
|
|
744
|
+
await runClaudeCode({
|
|
745
|
+
cwd: dir,
|
|
746
|
+
prompt: buildRepairPrompt(ctx, err.log),
|
|
747
|
+
permissionMode: ctx.permissionMode,
|
|
748
|
+
model: ORCHESTRATOR_MODEL,
|
|
749
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
652
754
|
function outcomeBanner(outcome, opts) {
|
|
653
755
|
const wikiNote = opts.documented ? " (with wiki updates)" : "";
|
|
654
756
|
switch (outcome.kind) {
|
|
@@ -670,20 +772,35 @@ function outcomeBanner(outcome, opts) {
|
|
|
670
772
|
}
|
|
671
773
|
}
|
|
672
774
|
async function processJob(ctx) {
|
|
673
|
-
const dir = await
|
|
775
|
+
const { dir, reused } = await acquireWorkspace(ctx.workspaceKey);
|
|
776
|
+
let prepared = false;
|
|
674
777
|
try {
|
|
675
|
-
if (ctx.kind === "init")
|
|
676
|
-
|
|
677
|
-
|
|
778
|
+
if (ctx.kind === "init") {
|
|
779
|
+
await prepareAtSha(ctx, dir, reused);
|
|
780
|
+
prepared = true;
|
|
781
|
+
return await processInitJob(ctx, dir);
|
|
782
|
+
}
|
|
783
|
+
if (ctx.kind === "implement") {
|
|
784
|
+
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
785
|
+
prepared = true;
|
|
786
|
+
return await processImplementJob(ctx, dir, resumed);
|
|
787
|
+
}
|
|
788
|
+
if (ctx.kind === "revise") {
|
|
789
|
+
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
790
|
+
prepared = true;
|
|
791
|
+
return await processReviseJob(ctx, dir, resumed);
|
|
792
|
+
}
|
|
793
|
+
await prepareAtSha(ctx, dir, reused);
|
|
794
|
+
prepared = true;
|
|
678
795
|
return await processChatJob(ctx, dir);
|
|
679
|
-
}
|
|
680
|
-
await
|
|
796
|
+
} catch (err) {
|
|
797
|
+
if (!prepared) await discardWorkspace(ctx.workspaceKey);
|
|
798
|
+
throw err;
|
|
681
799
|
}
|
|
682
800
|
}
|
|
683
801
|
async function processInitJob(ctx, dir) {
|
|
684
802
|
console.log(`
|
|
685
803
|
\u25B6 Init ${ctx.jobId} \u2014 ${ctx.repo.fullName} on ${ctx.repo.checkoutBranch}`);
|
|
686
|
-
await cloneAtSha(ctx, dir);
|
|
687
804
|
const summary = (await runClaudeCode({
|
|
688
805
|
cwd: dir,
|
|
689
806
|
prompt: buildInitPrompt(ctx),
|
|
@@ -701,7 +818,6 @@ async function processInitJob(ctx, dir) {
|
|
|
701
818
|
async function processChatJob(ctx, dir) {
|
|
702
819
|
console.log(`
|
|
703
820
|
\u25B6 Job ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
704
|
-
await cloneAtSha(ctx, dir);
|
|
705
821
|
const orchestrating = ctx.permissionMode !== "plan";
|
|
706
822
|
const installResult = orchestrating ? await installDependencies(dir) : null;
|
|
707
823
|
const result = await runClaudeCode({
|
|
@@ -743,10 +859,9 @@ async function processChatJob(ctx, dir) {
|
|
|
743
859
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
744
860
|
return { text: reply, widgets: [] };
|
|
745
861
|
}
|
|
746
|
-
async function processImplementJob(ctx, dir) {
|
|
862
|
+
async function processImplementJob(ctx, dir, resumed) {
|
|
747
863
|
console.log(`
|
|
748
864
|
\u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
749
|
-
const { resumed } = await cloneResumingBranch(ctx, dir);
|
|
750
865
|
const installResult = await installDependencies(dir);
|
|
751
866
|
const result = await runClaudeCode({
|
|
752
867
|
cwd: dir,
|
|
@@ -780,10 +895,9 @@ async function processImplementJob(ctx, dir) {
|
|
|
780
895
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
781
896
|
return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
|
|
782
897
|
}
|
|
783
|
-
async function processReviseJob(ctx, dir) {
|
|
898
|
+
async function processReviseJob(ctx, dir, resumed) {
|
|
784
899
|
console.log(`
|
|
785
900
|
\u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
786
|
-
const { resumed } = await cloneResumingBranch(ctx, dir);
|
|
787
901
|
const installResult = await installDependencies(dir);
|
|
788
902
|
const result = await runClaudeCode({
|
|
789
903
|
cwd: dir,
|
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
|
|