@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +134 -25
  3. 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 `runner-v*` tag is pushed.
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
- await git([
558
- "-C",
559
- dir,
560
- "-c",
561
- "user.email=runner@flumecode.local",
562
- "-c",
563
- "user.name=FlumeCode Runner",
564
- "commit",
565
- "--quiet",
566
- "-m",
567
- `FlumeCode: ${jobTitle(ctx)}`
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 commitChanges(ctx, dir);
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 makeWorkspace();
775
+ const { dir, reused } = await acquireWorkspace(ctx.workspaceKey);
776
+ let prepared = false;
679
777
  try {
680
- if (ctx.kind === "init") return await processInitJob(ctx, dir);
681
- if (ctx.kind === "implement") return await processImplementJob(ctx, dir);
682
- if (ctx.kind === "revise") return await processReviseJob(ctx, dir);
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
- } finally {
685
- await cleanup(dir);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flumecode/runner",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {