@flumecode/runner 0.3.2 → 0.5.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
|
@@ -97,6 +97,19 @@ async function claimJob(config) {
|
|
|
97
97
|
if (!res.ok) throw new Error(`claim failed: ${res.status} ${await safeText(res)}`);
|
|
98
98
|
return await res.json();
|
|
99
99
|
}
|
|
100
|
+
async function pollJobCanceling(config, jobId) {
|
|
101
|
+
const res = await fetch(`${config.serverUrl}/api/runner/jobs/${jobId}/cancel`, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: {
|
|
104
|
+
authorization: `Bearer ${config.token}`,
|
|
105
|
+
[RUNNER_VERSION_HEADER]: RUNNER_VERSION
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
noteServerVersion(res);
|
|
109
|
+
if (!res.ok) return false;
|
|
110
|
+
const body = await res.json();
|
|
111
|
+
return body.canceling === true;
|
|
112
|
+
}
|
|
100
113
|
async function reportJob(config, jobId, result) {
|
|
101
114
|
const res = await fetch(`${config.serverUrl}/api/runner/jobs/${jobId}/complete`, {
|
|
102
115
|
method: "POST",
|
|
@@ -328,6 +341,7 @@ async function runClaudeCode(opts) {
|
|
|
328
341
|
permissionMode: opts.permissionMode,
|
|
329
342
|
allowDangerouslySkipPermissions: opts.permissionMode === "bypassPermissions",
|
|
330
343
|
...opts.model ? { model: opts.model } : {},
|
|
344
|
+
...opts.abortController ? { abortController: opts.abortController } : {},
|
|
331
345
|
maxTurns: opts.maxTurns ?? 40,
|
|
332
346
|
// Hermetic: ignore ALL config from the checked-out repo (CLAUDE.md,
|
|
333
347
|
// .claude/settings.json, .claude/skills, …) and the runner owner's
|
|
@@ -358,6 +372,9 @@ async function runClaudeCode(opts) {
|
|
|
358
372
|
}
|
|
359
373
|
}
|
|
360
374
|
process.stdout.write("\n");
|
|
375
|
+
if (opts.abortController?.signal.aborted) {
|
|
376
|
+
throw new Error("Run canceled by user");
|
|
377
|
+
}
|
|
361
378
|
return { text: finalText, widgets: collected, plans: getPlans() };
|
|
362
379
|
}
|
|
363
380
|
|
|
@@ -424,6 +441,20 @@ function stripFrontMatter(raw) {
|
|
|
424
441
|
}
|
|
425
442
|
|
|
426
443
|
// src/prompt.ts
|
|
444
|
+
function turnHeading(turn, agentName) {
|
|
445
|
+
if (turn.role === "user") return "User";
|
|
446
|
+
if (turn.failed) return `${agentName} (this run ended in an error)`;
|
|
447
|
+
if (turn.kind === "plan") return `${agentName} (proposed a plan)`;
|
|
448
|
+
if (turn.kind === "report") return `${agentName} (implementation report)`;
|
|
449
|
+
return agentName;
|
|
450
|
+
}
|
|
451
|
+
function appendThread(lines, ctx) {
|
|
452
|
+
if (!ctx.thread || ctx.thread.length === 0) return;
|
|
453
|
+
lines.push("", "# Conversation so far");
|
|
454
|
+
for (const turn of ctx.thread) {
|
|
455
|
+
lines.push("", `## ${turnHeading(turn, ctx.agentName)}`, turn.content);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
427
458
|
function buildPrompt(ctx) {
|
|
428
459
|
const task = ctx.permissionMode === "plan" ? `Use the \`flumecode:request-to-plan\` skill to handle this request. You are read-only and cannot modify files \u2014 clarify any ambiguity with the user first, then produce a concrete, actionable plan (the specific changes you would make and why). Cite the relevant files. Do NOT call ExitPlanMode or write the plan to a file. When the plan is ready, call the \`submit_plan\` tool with the structured plan fields; the runner renders it into the canonical plan markdown and posts it as your comment.` : `Use the \`flumecode:implement-plan\` skill to handle this request. You are the ORCHESTRATOR: do not implement, review, or write the report yourself \u2014 follow the skill to delegate each phase to subagents via the Task tool, picking the right model for each. Do not commit or push \u2014 the runner handles that.`;
|
|
429
460
|
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 request. If there is no wiki, work from the code directly.`;
|
|
@@ -447,12 +478,7 @@ function buildPrompt(ctx) {
|
|
|
447
478
|
if (ctx.request?.body) {
|
|
448
479
|
lines.push("", ctx.request.body);
|
|
449
480
|
}
|
|
450
|
-
|
|
451
|
-
lines.push("", "# Conversation so far");
|
|
452
|
-
for (const turn of ctx.thread) {
|
|
453
|
-
lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
481
|
+
appendThread(lines, ctx);
|
|
456
482
|
lines.push(
|
|
457
483
|
"",
|
|
458
484
|
ctx.permissionMode === "plan" ? "Your final reply is posted verbatim as your comment in the thread \u2014 if you called `submit_plan`, the rendered plan is posted automatically; for clarifying questions, your reply text is posted as-is." : "Your final reply is posted verbatim as your comment in the thread \u2014 make it the implementation report your report subagent produced, with nothing added. The runner appends the pull-request link."
|
|
@@ -460,7 +486,7 @@ function buildPrompt(ctx) {
|
|
|
460
486
|
return lines.join("\n");
|
|
461
487
|
}
|
|
462
488
|
function buildRevisePrompt(ctx) {
|
|
463
|
-
const task = `Use the \`flumecode:revise-implementation\` skill to handle this turn. The plan below was already implemented (its report
|
|
489
|
+
const task = `Use the \`flumecode:revise-implementation\` skill to handle this turn. The plan below was already implemented (its implementation report appears in the conversation below, tagged as such); 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.`;
|
|
464
490
|
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.`;
|
|
465
491
|
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.`;
|
|
466
492
|
const lines = [
|
|
@@ -479,21 +505,39 @@ function buildRevisePrompt(ctx) {
|
|
|
479
505
|
if (ctx.request?.body) {
|
|
480
506
|
lines.push("", ctx.request.body);
|
|
481
507
|
}
|
|
482
|
-
|
|
483
|
-
lines.push("", "# Latest implementation report", "", ctx.priorReport);
|
|
484
|
-
}
|
|
485
|
-
if (ctx.thread && ctx.thread.length > 0) {
|
|
486
|
-
lines.push("", "# Conversation so far");
|
|
487
|
-
for (const turn of ctx.thread) {
|
|
488
|
-
lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
508
|
+
appendThread(lines, ctx);
|
|
491
509
|
lines.push(
|
|
492
510
|
"",
|
|
493
511
|
"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."
|
|
494
512
|
);
|
|
495
513
|
return lines.join("\n");
|
|
496
514
|
}
|
|
515
|
+
function buildResolvePrompt(ctx) {
|
|
516
|
+
const mergeBranch = ctx.repo.mergeBranch ?? "the merge branch";
|
|
517
|
+
const task = `Use the \`flumecode:resolve-merge-conflict\` skill to handle this turn. A merge of \`${mergeBranch}\` into this branch is IN PROGRESS and has left conflict markers in your working tree. Resolve every conflicted file by correctly integrating BOTH sides \u2014 the change this session implemented (described below) and the incoming changes from \`${mergeBranch}\` \u2014 never blindly discard either side. Remove all conflict markers and verify the result builds and tests pass. Do NOT \`git add\`, commit, push, or open a pull request \u2014 the runner finalizes the merge commit and updates the existing pull request.`;
|
|
518
|
+
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 the conflicting code. If there is no wiki, work from the code directly.`;
|
|
519
|
+
const lines = [
|
|
520
|
+
`You are "${ctx.agentName}", an autonomous coding agent resolving merge conflicts on an implemented FlumeCode plan.`,
|
|
521
|
+
`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 \u2014 with an in-progress merge of "${mergeBranch}".`,
|
|
522
|
+
task,
|
|
523
|
+
orient,
|
|
524
|
+
"",
|
|
525
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
526
|
+
"",
|
|
527
|
+
loadRule("coding-guideline"),
|
|
528
|
+
"",
|
|
529
|
+
`# Plan: ${ctx.request?.title ?? ""}`
|
|
530
|
+
];
|
|
531
|
+
if (ctx.request?.body) {
|
|
532
|
+
lines.push("", ctx.request.body);
|
|
533
|
+
}
|
|
534
|
+
appendThread(lines, ctx);
|
|
535
|
+
lines.push(
|
|
536
|
+
"",
|
|
537
|
+
"Resolve the conflicts now. Your final reply is posted as a report in the plan thread: summarize which files conflicted and how you resolved each (the runner appends the pull-request link, so don't add one)."
|
|
538
|
+
);
|
|
539
|
+
return lines.join("\n");
|
|
540
|
+
}
|
|
497
541
|
function buildDocumentPrompt(ctx) {
|
|
498
542
|
const lines = [
|
|
499
543
|
`You are "${ctx.agentName}" maintaining the repository wiki for ${ctx.repo.fullName}.`,
|
|
@@ -505,12 +549,7 @@ function buildDocumentPrompt(ctx) {
|
|
|
505
549
|
if (ctx.request?.body) {
|
|
506
550
|
lines.push("", ctx.request.body);
|
|
507
551
|
}
|
|
508
|
-
|
|
509
|
-
lines.push("", "# Conversation so far");
|
|
510
|
-
for (const turn of ctx.thread) {
|
|
511
|
-
lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
552
|
+
appendThread(lines, ctx);
|
|
514
553
|
lines.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
|
|
515
554
|
return lines.join("\n");
|
|
516
555
|
}
|
|
@@ -614,6 +653,12 @@ async function discardWorkspace(key) {
|
|
|
614
653
|
});
|
|
615
654
|
}
|
|
616
655
|
}
|
|
656
|
+
async function resetWorkspace(dir) {
|
|
657
|
+
await git(["-C", dir, "reset", "--hard", "HEAD"]).catch(() => {
|
|
658
|
+
});
|
|
659
|
+
await git(["-C", dir, "clean", "-fd"]).catch(() => {
|
|
660
|
+
});
|
|
661
|
+
}
|
|
617
662
|
async function prepareAtSha(ctx, dir, reused) {
|
|
618
663
|
if (!reused) {
|
|
619
664
|
await cloneAtSha(ctx, dir);
|
|
@@ -727,6 +772,33 @@ async function rebaseOntoMergeBranch(ctx, dir) {
|
|
|
727
772
|
throw new RebaseConflictError(mergeBranch, files);
|
|
728
773
|
}
|
|
729
774
|
}
|
|
775
|
+
async function mergeInMergeBranch(ctx, dir) {
|
|
776
|
+
const { mergeBranch } = ctx.repo;
|
|
777
|
+
if (!mergeBranch) return { conflicted: false };
|
|
778
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
|
|
779
|
+
try {
|
|
780
|
+
await git([
|
|
781
|
+
"-C",
|
|
782
|
+
dir,
|
|
783
|
+
"-c",
|
|
784
|
+
"user.email=runner@flumecode.local",
|
|
785
|
+
"-c",
|
|
786
|
+
"user.name=FlumeCode Runner",
|
|
787
|
+
"merge",
|
|
788
|
+
"--no-edit",
|
|
789
|
+
"FETCH_HEAD"
|
|
790
|
+
]);
|
|
791
|
+
return { conflicted: false };
|
|
792
|
+
} catch {
|
|
793
|
+
return { conflicted: true };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function listUnmergedPaths(dir) {
|
|
797
|
+
const { stdout: stdout2 } = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(() => ({
|
|
798
|
+
stdout: ""
|
|
799
|
+
}));
|
|
800
|
+
return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
801
|
+
}
|
|
730
802
|
async function openPullRequest(ctx) {
|
|
731
803
|
const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
|
|
732
804
|
if (!mergeBranch) return null;
|
|
@@ -772,21 +844,23 @@ async function cleanup(dir) {
|
|
|
772
844
|
|
|
773
845
|
// src/run.ts
|
|
774
846
|
var IDLE_MS = 5e3;
|
|
847
|
+
var CANCEL_POLL_MS = 2500;
|
|
775
848
|
var ORCHESTRATOR_MODEL = "sonnet";
|
|
776
849
|
var ORCHESTRATOR_MAX_TURNS = 80;
|
|
777
850
|
var MAX_COMMIT_REPAIRS = 2;
|
|
778
851
|
var INIT_MAX_TURNS = 200;
|
|
779
852
|
var DOCUMENT_MAX_TURNS = 120;
|
|
780
853
|
var HEARTBEAT_MS = 5 * 6e4;
|
|
781
|
-
async function pushAndOpenPr(ctx, dir, opts = { rebase: true }) {
|
|
782
|
-
|
|
854
|
+
async function pushAndOpenPr(ctx, dir, abort, opts = { rebase: true }) {
|
|
855
|
+
if (abort.signal.aborted) throw new Error("Run canceled by user");
|
|
856
|
+
const committed = await commitWithRepair(ctx, dir, abort);
|
|
783
857
|
if (!committed) return { kind: "none" };
|
|
784
858
|
if (opts.rebase) await rebaseOntoMergeBranch(ctx, dir);
|
|
785
859
|
await pushBranch(ctx, dir);
|
|
786
860
|
const pr = await openPullRequest(ctx);
|
|
787
861
|
return pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
788
862
|
}
|
|
789
|
-
async function commitWithRepair(ctx, dir) {
|
|
863
|
+
async function commitWithRepair(ctx, dir, abort) {
|
|
790
864
|
for (let attempt = 1; ; attempt++) {
|
|
791
865
|
try {
|
|
792
866
|
return await commitChanges(ctx, dir);
|
|
@@ -800,7 +874,8 @@ async function commitWithRepair(ctx, dir) {
|
|
|
800
874
|
prompt: buildRepairPrompt(ctx, err.log),
|
|
801
875
|
permissionMode: ctx.permissionMode,
|
|
802
876
|
model: ORCHESTRATOR_MODEL,
|
|
803
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
877
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
878
|
+
abortController: abort
|
|
804
879
|
});
|
|
805
880
|
}
|
|
806
881
|
}
|
|
@@ -825,51 +900,61 @@ function outcomeBanner(outcome, opts) {
|
|
|
825
900
|
\u2139\uFE0F **No code changes were made** \u2014 ${opts.noChange ?? "there was nothing to open a pull request for."}`;
|
|
826
901
|
}
|
|
827
902
|
}
|
|
828
|
-
async function processJob(ctx) {
|
|
903
|
+
async function processJob(ctx, abort = new AbortController()) {
|
|
829
904
|
const { dir, reused } = await acquireWorkspace(ctx.workspaceKey);
|
|
830
905
|
let prepared = false;
|
|
831
906
|
try {
|
|
832
907
|
if (ctx.kind === "init") {
|
|
833
908
|
await prepareAtSha(ctx, dir, reused);
|
|
834
909
|
prepared = true;
|
|
835
|
-
return await processInitJob(ctx, dir);
|
|
910
|
+
return await processInitJob(ctx, dir, abort);
|
|
836
911
|
}
|
|
837
912
|
if (ctx.kind === "implement") {
|
|
838
913
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
839
914
|
prepared = true;
|
|
840
|
-
return await processImplementJob(ctx, dir, resumed);
|
|
915
|
+
return await processImplementJob(ctx, dir, resumed, abort);
|
|
841
916
|
}
|
|
842
917
|
if (ctx.kind === "revise") {
|
|
843
918
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
844
919
|
prepared = true;
|
|
845
|
-
return await processReviseJob(ctx, dir, resumed);
|
|
920
|
+
return await processReviseJob(ctx, dir, resumed, abort);
|
|
921
|
+
}
|
|
922
|
+
if (ctx.kind === "resolve") {
|
|
923
|
+
await prepareResumingBranch(ctx, dir, reused);
|
|
924
|
+
prepared = true;
|
|
925
|
+
return await processResolveJob(ctx, dir, abort);
|
|
846
926
|
}
|
|
847
927
|
await prepareAtSha(ctx, dir, reused);
|
|
848
928
|
prepared = true;
|
|
849
|
-
return await processChatJob(ctx, dir);
|
|
929
|
+
return await processChatJob(ctx, dir, abort);
|
|
850
930
|
} catch (err) {
|
|
851
|
-
if (
|
|
931
|
+
if (abort.signal.aborted && prepared) {
|
|
932
|
+
await resetWorkspace(dir);
|
|
933
|
+
} else if (!prepared) {
|
|
934
|
+
await discardWorkspace(ctx.workspaceKey);
|
|
935
|
+
}
|
|
852
936
|
throw err;
|
|
853
937
|
}
|
|
854
938
|
}
|
|
855
|
-
async function processInitJob(ctx, dir) {
|
|
939
|
+
async function processInitJob(ctx, dir, abort) {
|
|
856
940
|
console.log(`
|
|
857
941
|
\u25B6 Init ${ctx.jobId} \u2014 ${ctx.repo.fullName} on ${ctx.repo.checkoutBranch}`);
|
|
858
942
|
const summary = (await runClaudeCode({
|
|
859
943
|
cwd: dir,
|
|
860
944
|
prompt: buildInitPrompt(ctx),
|
|
861
945
|
permissionMode: ctx.permissionMode,
|
|
862
|
-
maxTurns: INIT_MAX_TURNS
|
|
946
|
+
maxTurns: INIT_MAX_TURNS,
|
|
947
|
+
abortController: abort
|
|
863
948
|
})).text.trim();
|
|
864
949
|
let reply = summary || "(the agent produced no summary)";
|
|
865
|
-
const outcome = await pushAndOpenPr(ctx, dir);
|
|
950
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort);
|
|
866
951
|
reply += outcomeBanner(outcome, {
|
|
867
952
|
branch: ctx.repo.checkoutBranch,
|
|
868
953
|
noChange: "no files were generated; the wiki may already exist."
|
|
869
954
|
});
|
|
870
955
|
return { text: reply, widgets: [] };
|
|
871
956
|
}
|
|
872
|
-
async function processChatJob(ctx, dir) {
|
|
957
|
+
async function processChatJob(ctx, dir, abort) {
|
|
873
958
|
console.log(`
|
|
874
959
|
\u25B6 Job ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
875
960
|
const orchestrating = ctx.permissionMode !== "plan";
|
|
@@ -878,6 +963,7 @@ async function processChatJob(ctx, dir) {
|
|
|
878
963
|
cwd: dir,
|
|
879
964
|
prompt: buildPrompt(ctx),
|
|
880
965
|
permissionMode: ctx.permissionMode,
|
|
966
|
+
abortController: abort,
|
|
881
967
|
...orchestrating ? { model: ORCHESTRATOR_MODEL, maxTurns: ORCHESTRATOR_MAX_TURNS } : {}
|
|
882
968
|
});
|
|
883
969
|
const summary = result.text.trim();
|
|
@@ -902,18 +988,19 @@ async function processChatJob(ctx, dir) {
|
|
|
902
988
|
cwd: dir,
|
|
903
989
|
prompt: buildDocumentPrompt(ctx),
|
|
904
990
|
permissionMode: ctx.permissionMode,
|
|
905
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
991
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
992
|
+
abortController: abort
|
|
906
993
|
});
|
|
907
994
|
documented = true;
|
|
908
995
|
} catch (err) {
|
|
909
996
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
910
997
|
}
|
|
911
998
|
}
|
|
912
|
-
const outcome = await pushAndOpenPr(ctx, dir);
|
|
999
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort);
|
|
913
1000
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
914
1001
|
return { text: reply, widgets: [] };
|
|
915
1002
|
}
|
|
916
|
-
async function processImplementJob(ctx, dir, resumed) {
|
|
1003
|
+
async function processImplementJob(ctx, dir, resumed, abort) {
|
|
917
1004
|
console.log(`
|
|
918
1005
|
\u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
919
1006
|
const installResult = await installDependencies(dir);
|
|
@@ -922,7 +1009,8 @@ async function processImplementJob(ctx, dir, resumed) {
|
|
|
922
1009
|
prompt: buildPrompt(ctx),
|
|
923
1010
|
permissionMode: ctx.permissionMode,
|
|
924
1011
|
model: ORCHESTRATOR_MODEL,
|
|
925
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
1012
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1013
|
+
abortController: abort
|
|
926
1014
|
});
|
|
927
1015
|
let reply = result.text.trim() || "(the agent produced no report)";
|
|
928
1016
|
if (installResult.status === "failed") {
|
|
@@ -938,18 +1026,19 @@ async function processImplementJob(ctx, dir, resumed) {
|
|
|
938
1026
|
cwd: dir,
|
|
939
1027
|
prompt: buildDocumentPrompt(ctx),
|
|
940
1028
|
permissionMode: ctx.permissionMode,
|
|
941
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
1029
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
1030
|
+
abortController: abort
|
|
942
1031
|
});
|
|
943
1032
|
documented = true;
|
|
944
1033
|
} catch (err) {
|
|
945
1034
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
946
1035
|
}
|
|
947
1036
|
}
|
|
948
|
-
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
1037
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
|
|
949
1038
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
950
1039
|
return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
|
|
951
1040
|
}
|
|
952
|
-
async function processReviseJob(ctx, dir, resumed) {
|
|
1041
|
+
async function processReviseJob(ctx, dir, resumed, abort) {
|
|
953
1042
|
console.log(`
|
|
954
1043
|
\u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
955
1044
|
const installResult = await installDependencies(dir);
|
|
@@ -958,7 +1047,8 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
958
1047
|
prompt: buildRevisePrompt(ctx),
|
|
959
1048
|
permissionMode: ctx.permissionMode,
|
|
960
1049
|
model: ORCHESTRATOR_MODEL,
|
|
961
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
1050
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1051
|
+
abortController: abort
|
|
962
1052
|
});
|
|
963
1053
|
const summary = result.text.trim();
|
|
964
1054
|
let reply = summary || "(the agent produced no reply)";
|
|
@@ -980,14 +1070,15 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
980
1070
|
cwd: dir,
|
|
981
1071
|
prompt: buildDocumentPrompt(ctx),
|
|
982
1072
|
permissionMode: ctx.permissionMode,
|
|
983
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
1073
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
1074
|
+
abortController: abort
|
|
984
1075
|
});
|
|
985
1076
|
documented = true;
|
|
986
1077
|
} catch (err) {
|
|
987
1078
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
988
1079
|
}
|
|
989
1080
|
}
|
|
990
|
-
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
1081
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
|
|
991
1082
|
if (outcome.kind !== "none") {
|
|
992
1083
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
993
1084
|
}
|
|
@@ -998,6 +1089,44 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
998
1089
|
...result.plans?.length ? { plans: result.plans } : {}
|
|
999
1090
|
};
|
|
1000
1091
|
}
|
|
1092
|
+
async function processResolveJob(ctx, dir, abort) {
|
|
1093
|
+
console.log(`
|
|
1094
|
+
\u25B6 Resolve ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1095
|
+
const installResult = await installDependencies(dir);
|
|
1096
|
+
const { conflicted } = await mergeInMergeBranch(ctx, dir);
|
|
1097
|
+
let reply;
|
|
1098
|
+
if (conflicted) {
|
|
1099
|
+
const result = await runClaudeCode({
|
|
1100
|
+
cwd: dir,
|
|
1101
|
+
prompt: buildResolvePrompt(ctx),
|
|
1102
|
+
permissionMode: ctx.permissionMode,
|
|
1103
|
+
model: ORCHESTRATOR_MODEL,
|
|
1104
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1105
|
+
abortController: abort
|
|
1106
|
+
});
|
|
1107
|
+
reply = result.text.trim() || "(the agent produced no report)";
|
|
1108
|
+
const unresolved = await listUnmergedPaths(dir);
|
|
1109
|
+
if (unresolved.length > 0) {
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
`Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still conflict: ${unresolved.join(", ")}`
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
} else {
|
|
1115
|
+
reply = `Merged \`${ctx.repo.mergeBranch ?? "the merge branch"}\` into \`${ctx.repo.checkoutBranch}\` cleanly \u2014 there were no conflicts to resolve.`;
|
|
1116
|
+
}
|
|
1117
|
+
if (installResult.status === "failed") {
|
|
1118
|
+
reply += `
|
|
1119
|
+
|
|
1120
|
+
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
1121
|
+
}
|
|
1122
|
+
if (abort.signal.aborted) throw new Error("Run canceled by user");
|
|
1123
|
+
await commitWithRepair(ctx, dir, abort);
|
|
1124
|
+
await pushBranch(ctx, dir);
|
|
1125
|
+
const pr = await openPullRequest(ctx);
|
|
1126
|
+
const outcome = pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
1127
|
+
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch });
|
|
1128
|
+
return { text: reply, widgets: [], ...pr ? { pr } : {} };
|
|
1129
|
+
}
|
|
1001
1130
|
async function heartbeat(config) {
|
|
1002
1131
|
const health = await checkClaudeCode();
|
|
1003
1132
|
if (health.ready) {
|
|
@@ -1016,43 +1145,78 @@ async function pollLoop(config) {
|
|
|
1016
1145
|
const swept = await sweepWorkspaces();
|
|
1017
1146
|
if (swept > 0) console.log(`Cleared ${swept} stale workspace${swept === 1 ? "" : "s"}.`);
|
|
1018
1147
|
await heartbeat(config);
|
|
1019
|
-
let
|
|
1020
|
-
|
|
1021
|
-
|
|
1148
|
+
let heartbeatTimer;
|
|
1149
|
+
const scheduleHeartbeat = () => {
|
|
1150
|
+
heartbeatTimer = setTimeout(async () => {
|
|
1022
1151
|
await heartbeat(config);
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
await sleep(IDLE_MS);
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
if (!ctx) {
|
|
1034
|
-
await sleep(IDLE_MS);
|
|
1035
|
-
continue;
|
|
1036
|
-
}
|
|
1037
|
-
try {
|
|
1038
|
-
const { text, widgets, pr, plans } = await processJob(ctx);
|
|
1039
|
-
await reportJob(config, ctx.jobId, {
|
|
1040
|
-
status: "done",
|
|
1041
|
-
text,
|
|
1042
|
-
widgets,
|
|
1043
|
-
pr,
|
|
1044
|
-
...plans?.length ? { plans } : {}
|
|
1045
|
-
});
|
|
1046
|
-
console.log(`\u2713 Job ${ctx.jobId} done`);
|
|
1047
|
-
} catch (err) {
|
|
1048
|
-
const message = errorMessage2(err);
|
|
1049
|
-
console.error(`\u2717 Job ${ctx.jobId} failed: ${message}`);
|
|
1152
|
+
scheduleHeartbeat();
|
|
1153
|
+
}, HEARTBEAT_MS);
|
|
1154
|
+
};
|
|
1155
|
+
scheduleHeartbeat();
|
|
1156
|
+
try {
|
|
1157
|
+
for (; ; ) {
|
|
1158
|
+
let ctx = null;
|
|
1050
1159
|
try {
|
|
1051
|
-
await
|
|
1052
|
-
} catch (
|
|
1053
|
-
console.error(`
|
|
1160
|
+
ctx = await claimJob(config);
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
console.error(`Claim error: ${errorMessage2(err)}`);
|
|
1163
|
+
await sleep(IDLE_MS);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if (!ctx) {
|
|
1167
|
+
await sleep(IDLE_MS);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
const abort = new AbortController();
|
|
1171
|
+
let stopPolling = false;
|
|
1172
|
+
const scheduleCancelPoll = () => {
|
|
1173
|
+
setTimeout(async () => {
|
|
1174
|
+
if (stopPolling) return;
|
|
1175
|
+
try {
|
|
1176
|
+
if (await pollJobCanceling(config, ctx.jobId)) {
|
|
1177
|
+
console.log(`\u23F9 Stop requested for job ${ctx.jobId} \u2014 aborting\u2026`);
|
|
1178
|
+
abort.abort();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
} catch {
|
|
1182
|
+
}
|
|
1183
|
+
if (!stopPolling) scheduleCancelPoll();
|
|
1184
|
+
}, CANCEL_POLL_MS);
|
|
1185
|
+
};
|
|
1186
|
+
scheduleCancelPoll();
|
|
1187
|
+
try {
|
|
1188
|
+
const { text, widgets, pr, plans } = await processJob(ctx, abort);
|
|
1189
|
+
await reportJob(config, ctx.jobId, {
|
|
1190
|
+
status: "done",
|
|
1191
|
+
text,
|
|
1192
|
+
widgets,
|
|
1193
|
+
pr,
|
|
1194
|
+
...plans?.length ? { plans } : {}
|
|
1195
|
+
});
|
|
1196
|
+
console.log(`\u2713 Job ${ctx.jobId} done`);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
if (abort.signal.aborted) {
|
|
1199
|
+
console.log(`\u23F9 Job ${ctx.jobId} canceled`);
|
|
1200
|
+
try {
|
|
1201
|
+
await reportJob(config, ctx.jobId, { status: "canceled" });
|
|
1202
|
+
} catch (reportErr) {
|
|
1203
|
+
console.error(` (failed to report the cancellation: ${errorMessage2(reportErr)})`);
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
const message = errorMessage2(err);
|
|
1207
|
+
console.error(`\u2717 Job ${ctx.jobId} failed: ${message}`);
|
|
1208
|
+
try {
|
|
1209
|
+
await reportJob(config, ctx.jobId, { status: "error", error: message });
|
|
1210
|
+
} catch (reportErr) {
|
|
1211
|
+
console.error(` (also failed to report the error: ${errorMessage2(reportErr)})`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
} finally {
|
|
1215
|
+
stopPolling = true;
|
|
1054
1216
|
}
|
|
1055
1217
|
}
|
|
1218
|
+
} finally {
|
|
1219
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
1056
1220
|
}
|
|
1057
1221
|
}
|
|
1058
1222
|
function sleep(ms) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resolve-merge-conflict
|
|
3
|
+
description: >-
|
|
4
|
+
Resolve the merge conflicts left by an in-progress merge of a coding session's
|
|
5
|
+
merge branch into its pull-request branch. Use in resolve-job runs. The working
|
|
6
|
+
tree already has conflict markers; integrate both sides of every conflict
|
|
7
|
+
correctly, verify the build/tests, and leave the resolved changes in the working
|
|
8
|
+
tree. Never `git add`, commit, push, or open a PR — the runner finalizes the merge.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# resolve-merge-conflict
|
|
12
|
+
|
|
13
|
+
The plan in the context above was **already implemented** and has an open pull
|
|
14
|
+
request. Its merge branch (e.g. `main`) has since advanced and now **conflicts** with
|
|
15
|
+
the PR branch. The runner has already started a `git merge` of the merge branch into
|
|
16
|
+
your current branch, so your working tree contains the **conflict markers**
|
|
17
|
+
(`<<<<<<<`, `=======`, `>>>>>>>`). Your job is to resolve every conflict so the PR
|
|
18
|
+
becomes mergeable again — without losing this session's work or the incoming changes.
|
|
19
|
+
|
|
20
|
+
## You are stateless — orient yourself first
|
|
21
|
+
|
|
22
|
+
You see the whole session thread (the plan and its implementation report) but keep
|
|
23
|
+
**no memory** between turns. Read it first: the plan and report tell you what this
|
|
24
|
+
branch was building, which is exactly the intent you must preserve when you choose how
|
|
25
|
+
to integrate each conflict. Check the FlumeCode wiki (`.flumecode/wiki/README.md`) if
|
|
26
|
+
one exists to understand the conflicting code.
|
|
27
|
+
|
|
28
|
+
## Step 1 — Find every conflict
|
|
29
|
+
|
|
30
|
+
- `git status` shows the unmerged paths; `git diff --diff-filter=U` shows the
|
|
31
|
+
conflicting hunks. Work through **all** of them — a single leftover marker fails the
|
|
32
|
+
run.
|
|
33
|
+
|
|
34
|
+
## Step 2 — Resolve each conflict correctly
|
|
35
|
+
|
|
36
|
+
For each conflicted file, understand **both** sides before editing:
|
|
37
|
+
|
|
38
|
+
- **Ours** (the conflict's first side) is this session's implementation — the change
|
|
39
|
+
the plan/report describes.
|
|
40
|
+
- **Theirs** (the second side) is what landed on the merge branch since.
|
|
41
|
+
|
|
42
|
+
Integrate them so **both** intents survive. Do **not** blindly pick a side or delete
|
|
43
|
+
one half to make the markers go away — that silently drops work. When the two changes
|
|
44
|
+
are genuinely incompatible, prefer keeping this session's implementation intent while
|
|
45
|
+
adapting it to the incoming code (e.g. a renamed function, a moved file, a new
|
|
46
|
+
signature). Remove every conflict marker; the file must read as clean, correct code.
|
|
47
|
+
|
|
48
|
+
You may delegate isolated, well-scoped resolutions to **Task** subagents, but give
|
|
49
|
+
each a self-contained prompt (the conflicting file's content, both intents, and the
|
|
50
|
+
coding guidelines verbatim) — subagents start blank.
|
|
51
|
+
|
|
52
|
+
## Step 3 — Verify
|
|
53
|
+
|
|
54
|
+
Run the project's build and tests to confirm the resolved tree is correct (the
|
|
55
|
+
runner's commit will also run the repo's pre-commit hook, so failing checks would
|
|
56
|
+
block the merge anyway). Fix anything the merge broke.
|
|
57
|
+
|
|
58
|
+
## Never
|
|
59
|
+
|
|
60
|
+
- Never `git add`, `git commit`, `git merge --continue`, `git push`, or open a PR —
|
|
61
|
+
the runner stages the resolved tree, finalizes the merge commit, and updates the
|
|
62
|
+
**existing** pull request. A new PR must never be created.
|
|
63
|
+
- Never leave a conflict marker behind, and never resolve a conflict by discarding one
|
|
64
|
+
side's intent.
|
|
65
|
+
|
|
66
|
+
## Your final reply
|
|
67
|
+
|
|
68
|
+
Your last message **is** the report posted to the session thread. Write it for the
|
|
69
|
+
user: list which files conflicted and, briefly, how you resolved each, plus how you
|
|
70
|
+
verified (build/tests). The runner appends the pull-request link, so don't add one.
|