@flumecode/runner 0.2.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 +134 -25
- package/package.json +1 -1
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
|
@@ -460,6 +460,25 @@ function buildDocumentPrompt(ctx) {
|
|
|
460
460
|
lines.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
|
|
461
461
|
return lines.join("\n");
|
|
462
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
|
+
}
|
|
463
482
|
function buildInitPrompt(ctx) {
|
|
464
483
|
return [
|
|
465
484
|
`You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
@@ -513,6 +532,48 @@ async function installDependencies(dir) {
|
|
|
513
532
|
async function makeWorkspace() {
|
|
514
533
|
return mkdtemp(join3(tmpdir(), WORKSPACE_PREFIX));
|
|
515
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
|
+
}
|
|
516
577
|
async function sweepWorkspaces() {
|
|
517
578
|
const base = tmpdir();
|
|
518
579
|
let entries;
|
|
@@ -552,20 +613,36 @@ async function hasChanges(dir) {
|
|
|
552
613
|
const { stdout: stdout2 } = await git(["-C", dir, "status", "--porcelain"]);
|
|
553
614
|
return stdout2.trim().length > 0;
|
|
554
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
|
+
}
|
|
555
628
|
async function commitChanges(ctx, dir) {
|
|
556
629
|
if (!await hasChanges(dir)) return false;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
+
}
|
|
569
646
|
return true;
|
|
570
647
|
}
|
|
571
648
|
async function pushBranch(ctx, dir) {
|
|
@@ -643,17 +720,37 @@ async function cleanup(dir) {
|
|
|
643
720
|
var IDLE_MS = 5e3;
|
|
644
721
|
var ORCHESTRATOR_MODEL = "sonnet";
|
|
645
722
|
var ORCHESTRATOR_MAX_TURNS = 80;
|
|
723
|
+
var MAX_COMMIT_REPAIRS = 2;
|
|
646
724
|
var INIT_MAX_TURNS = 200;
|
|
647
725
|
var DOCUMENT_MAX_TURNS = 120;
|
|
648
726
|
var HEARTBEAT_MS = 5 * 6e4;
|
|
649
727
|
async function pushAndOpenPr(ctx, dir, opts = { rebase: true }) {
|
|
650
|
-
const committed = await
|
|
728
|
+
const committed = await commitWithRepair(ctx, dir);
|
|
651
729
|
if (!committed) return { kind: "none" };
|
|
652
730
|
if (opts.rebase) await rebaseOntoMergeBranch(ctx, dir);
|
|
653
731
|
await pushBranch(ctx, dir);
|
|
654
732
|
const pr = await openPullRequest(ctx);
|
|
655
733
|
return pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
656
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
|
+
}
|
|
657
754
|
function outcomeBanner(outcome, opts) {
|
|
658
755
|
const wikiNote = opts.documented ? " (with wiki updates)" : "";
|
|
659
756
|
switch (outcome.kind) {
|
|
@@ -675,20 +772,35 @@ function outcomeBanner(outcome, opts) {
|
|
|
675
772
|
}
|
|
676
773
|
}
|
|
677
774
|
async function processJob(ctx) {
|
|
678
|
-
const dir = await
|
|
775
|
+
const { dir, reused } = await acquireWorkspace(ctx.workspaceKey);
|
|
776
|
+
let prepared = false;
|
|
679
777
|
try {
|
|
680
|
-
if (ctx.kind === "init")
|
|
681
|
-
|
|
682
|
-
|
|
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;
|
|
683
795
|
return await processChatJob(ctx, dir);
|
|
684
|
-
}
|
|
685
|
-
await
|
|
796
|
+
} catch (err) {
|
|
797
|
+
if (!prepared) await discardWorkspace(ctx.workspaceKey);
|
|
798
|
+
throw err;
|
|
686
799
|
}
|
|
687
800
|
}
|
|
688
801
|
async function processInitJob(ctx, dir) {
|
|
689
802
|
console.log(`
|
|
690
803
|
\u25B6 Init ${ctx.jobId} \u2014 ${ctx.repo.fullName} on ${ctx.repo.checkoutBranch}`);
|
|
691
|
-
await cloneAtSha(ctx, dir);
|
|
692
804
|
const summary = (await runClaudeCode({
|
|
693
805
|
cwd: dir,
|
|
694
806
|
prompt: buildInitPrompt(ctx),
|
|
@@ -706,7 +818,6 @@ async function processInitJob(ctx, dir) {
|
|
|
706
818
|
async function processChatJob(ctx, dir) {
|
|
707
819
|
console.log(`
|
|
708
820
|
\u25B6 Job ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
709
|
-
await cloneAtSha(ctx, dir);
|
|
710
821
|
const orchestrating = ctx.permissionMode !== "plan";
|
|
711
822
|
const installResult = orchestrating ? await installDependencies(dir) : null;
|
|
712
823
|
const result = await runClaudeCode({
|
|
@@ -748,10 +859,9 @@ async function processChatJob(ctx, dir) {
|
|
|
748
859
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
749
860
|
return { text: reply, widgets: [] };
|
|
750
861
|
}
|
|
751
|
-
async function processImplementJob(ctx, dir) {
|
|
862
|
+
async function processImplementJob(ctx, dir, resumed) {
|
|
752
863
|
console.log(`
|
|
753
864
|
\u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
754
|
-
const { resumed } = await cloneResumingBranch(ctx, dir);
|
|
755
865
|
const installResult = await installDependencies(dir);
|
|
756
866
|
const result = await runClaudeCode({
|
|
757
867
|
cwd: dir,
|
|
@@ -785,10 +895,9 @@ async function processImplementJob(ctx, dir) {
|
|
|
785
895
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
786
896
|
return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
|
|
787
897
|
}
|
|
788
|
-
async function processReviseJob(ctx, dir) {
|
|
898
|
+
async function processReviseJob(ctx, dir, resumed) {
|
|
789
899
|
console.log(`
|
|
790
900
|
\u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
791
|
-
const { resumed } = await cloneResumingBranch(ctx, dir);
|
|
792
901
|
const installResult = await installDependencies(dir);
|
|
793
902
|
const result = await runClaudeCode({
|
|
794
903
|
cwd: dir,
|