@flumecode/runner 0.3.2 → 0.6.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
|
}
|
|
@@ -533,6 +572,33 @@ function buildRepairPrompt(ctx, hookLog) {
|
|
|
533
572
|
];
|
|
534
573
|
return lines.join("\n");
|
|
535
574
|
}
|
|
575
|
+
function buildReleasePrompt(ctx) {
|
|
576
|
+
const task = `Use the \`flumecode:create-release\` skill to handle this turn. You are driving a release: first analyse commits since the last tag, propose version bumps, and ask the user to confirm via widgets (Phase 1); once the user's widget answers appear in the thread, apply the bumps to package.json files and update CHANGELOG.md (Phase 2). Do NOT commit or push \u2014 the runner handles that and opens the bump PR.`;
|
|
577
|
+
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 release. If there is no wiki, work from the code directly.`;
|
|
578
|
+
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.`;
|
|
579
|
+
const lines = [
|
|
580
|
+
`You are "${ctx.agentName}", an autonomous coding agent driving a FlumeCode release.`,
|
|
581
|
+
`The repository ${ctx.repo.fullName} is checked out in your current working directory on the release bump branch "${ctx.repo.checkoutBranch}".`,
|
|
582
|
+
task,
|
|
583
|
+
orient,
|
|
584
|
+
widgets,
|
|
585
|
+
"",
|
|
586
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
587
|
+
"",
|
|
588
|
+
loadRule("coding-guideline"),
|
|
589
|
+
"",
|
|
590
|
+
`# Release: ${ctx.request?.title ?? ""}`
|
|
591
|
+
];
|
|
592
|
+
if (ctx.request?.body) {
|
|
593
|
+
lines.push("", ctx.request.body);
|
|
594
|
+
}
|
|
595
|
+
appendThread(lines, ctx);
|
|
596
|
+
lines.push(
|
|
597
|
+
"",
|
|
598
|
+
"Your final reply is posted verbatim as your comment in the release thread \u2014 if you called widgets (Phase 1), your reply text accompanies the questions; if you applied the bumps (Phase 2), make it the report the skill produced. The runner appends the pull-request link."
|
|
599
|
+
);
|
|
600
|
+
return lines.join("\n");
|
|
601
|
+
}
|
|
536
602
|
function buildInitPrompt(ctx) {
|
|
537
603
|
return [
|
|
538
604
|
`You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
@@ -614,6 +680,12 @@ async function discardWorkspace(key) {
|
|
|
614
680
|
});
|
|
615
681
|
}
|
|
616
682
|
}
|
|
683
|
+
async function resetWorkspace(dir) {
|
|
684
|
+
await git(["-C", dir, "reset", "--hard", "HEAD"]).catch(() => {
|
|
685
|
+
});
|
|
686
|
+
await git(["-C", dir, "clean", "-fd"]).catch(() => {
|
|
687
|
+
});
|
|
688
|
+
}
|
|
617
689
|
async function prepareAtSha(ctx, dir, reused) {
|
|
618
690
|
if (!reused) {
|
|
619
691
|
await cloneAtSha(ctx, dir);
|
|
@@ -716,17 +788,45 @@ async function rebaseOntoMergeBranch(ctx, dir) {
|
|
|
716
788
|
if (!mergeBranch) return;
|
|
717
789
|
await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
|
|
718
790
|
try {
|
|
719
|
-
await git(["-C", dir, "rebase", "FETCH_HEAD"]);
|
|
720
|
-
} catch {
|
|
791
|
+
await git(["-C", dir, "rebase", "--empty=drop", "FETCH_HEAD"]);
|
|
792
|
+
} catch (err) {
|
|
721
793
|
const conflicted = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(
|
|
722
794
|
() => ({ stdout: "" })
|
|
723
795
|
);
|
|
724
796
|
const files = conflicted.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
725
797
|
await git(["-C", dir, "rebase", "--abort"]).catch(() => {
|
|
726
798
|
});
|
|
799
|
+
if (files.length === 0) throw err;
|
|
727
800
|
throw new RebaseConflictError(mergeBranch, files);
|
|
728
801
|
}
|
|
729
802
|
}
|
|
803
|
+
async function mergeInMergeBranch(ctx, dir) {
|
|
804
|
+
const { mergeBranch } = ctx.repo;
|
|
805
|
+
if (!mergeBranch) return { conflicted: false };
|
|
806
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
|
|
807
|
+
try {
|
|
808
|
+
await git([
|
|
809
|
+
"-C",
|
|
810
|
+
dir,
|
|
811
|
+
"-c",
|
|
812
|
+
"user.email=runner@flumecode.local",
|
|
813
|
+
"-c",
|
|
814
|
+
"user.name=FlumeCode Runner",
|
|
815
|
+
"merge",
|
|
816
|
+
"--no-edit",
|
|
817
|
+
"FETCH_HEAD"
|
|
818
|
+
]);
|
|
819
|
+
return { conflicted: false };
|
|
820
|
+
} catch {
|
|
821
|
+
return { conflicted: true };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function listUnmergedPaths(dir) {
|
|
825
|
+
const { stdout: stdout2 } = await git(["-C", dir, "diff", "--name-only", "--diff-filter=U"]).catch(() => ({
|
|
826
|
+
stdout: ""
|
|
827
|
+
}));
|
|
828
|
+
return stdout2.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
829
|
+
}
|
|
730
830
|
async function openPullRequest(ctx) {
|
|
731
831
|
const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
|
|
732
832
|
if (!mergeBranch) return null;
|
|
@@ -772,21 +872,56 @@ async function cleanup(dir) {
|
|
|
772
872
|
|
|
773
873
|
// src/run.ts
|
|
774
874
|
var IDLE_MS = 5e3;
|
|
875
|
+
var CANCEL_POLL_MS = 2500;
|
|
775
876
|
var ORCHESTRATOR_MODEL = "sonnet";
|
|
776
877
|
var ORCHESTRATOR_MAX_TURNS = 80;
|
|
777
878
|
var MAX_COMMIT_REPAIRS = 2;
|
|
778
879
|
var INIT_MAX_TURNS = 200;
|
|
779
880
|
var DOCUMENT_MAX_TURNS = 120;
|
|
780
881
|
var HEARTBEAT_MS = 5 * 6e4;
|
|
781
|
-
async function pushAndOpenPr(ctx, dir, opts = { rebase: true }) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (
|
|
882
|
+
async function pushAndOpenPr(ctx, dir, abort, opts = { rebase: true }) {
|
|
883
|
+
if (abort.signal.aborted) throw new Error("Run canceled by user");
|
|
884
|
+
const committed = await commitWithRepair(ctx, dir, abort);
|
|
885
|
+
if (!committed) return { outcome: { kind: "none" }, autoMerged: false };
|
|
886
|
+
let autoMerged = false;
|
|
887
|
+
if (opts.rebase) {
|
|
888
|
+
try {
|
|
889
|
+
await rebaseOntoMergeBranch(ctx, dir);
|
|
890
|
+
} catch (err) {
|
|
891
|
+
if (!(err instanceof RebaseConflictError)) throw err;
|
|
892
|
+
if (abort.signal.aborted) throw new Error("Run canceled by user");
|
|
893
|
+
console.warn(
|
|
894
|
+
` rebase onto ${ctx.repo.mergeBranch} conflicted \u2014 merging it in and resolving with the agent\u2026`
|
|
895
|
+
);
|
|
896
|
+
await mergeAndResolveConflicts(ctx, dir, abort);
|
|
897
|
+
await commitWithRepair(ctx, dir, abort);
|
|
898
|
+
autoMerged = true;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
785
901
|
await pushBranch(ctx, dir);
|
|
786
902
|
const pr = await openPullRequest(ctx);
|
|
787
|
-
return pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
903
|
+
return { outcome: pr ? { kind: "pr", pr } : { kind: "pushed" }, autoMerged };
|
|
788
904
|
}
|
|
789
|
-
async function
|
|
905
|
+
async function mergeAndResolveConflicts(ctx, dir, abort) {
|
|
906
|
+
const { conflicted } = await mergeInMergeBranch(ctx, dir);
|
|
907
|
+
if (!conflicted) return { resolved: false, text: null };
|
|
908
|
+
const result = await runClaudeCode({
|
|
909
|
+
cwd: dir,
|
|
910
|
+
prompt: buildResolvePrompt(ctx),
|
|
911
|
+
permissionMode: ctx.permissionMode,
|
|
912
|
+
model: ORCHESTRATOR_MODEL,
|
|
913
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
914
|
+
abortController: abort
|
|
915
|
+
});
|
|
916
|
+
const unresolved = await listUnmergedPaths(dir);
|
|
917
|
+
if (unresolved.length > 0) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
`Could not fully resolve the merge \u2014 ${unresolved.length} file(s) still conflict: ${unresolved.join(", ")}`
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
return { resolved: true, text: result.text.trim() || null };
|
|
923
|
+
}
|
|
924
|
+
async function commitWithRepair(ctx, dir, abort) {
|
|
790
925
|
for (let attempt = 1; ; attempt++) {
|
|
791
926
|
try {
|
|
792
927
|
return await commitChanges(ctx, dir);
|
|
@@ -800,24 +935,26 @@ async function commitWithRepair(ctx, dir) {
|
|
|
800
935
|
prompt: buildRepairPrompt(ctx, err.log),
|
|
801
936
|
permissionMode: ctx.permissionMode,
|
|
802
937
|
model: ORCHESTRATOR_MODEL,
|
|
803
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
938
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
939
|
+
abortController: abort
|
|
804
940
|
});
|
|
805
941
|
}
|
|
806
942
|
}
|
|
807
943
|
}
|
|
808
944
|
function outcomeBanner(outcome, opts) {
|
|
809
945
|
const wikiNote = opts.documented ? " (with wiki updates)" : "";
|
|
946
|
+
const mergeNote = opts.autoMerged ? "\n\u{1F500} The base branch had advanced with conflicting changes \u2014 merged it in and resolved the conflicts automatically." : "";
|
|
810
947
|
switch (outcome.kind) {
|
|
811
948
|
case "pr":
|
|
812
949
|
return `
|
|
813
950
|
|
|
814
951
|
---
|
|
815
|
-
\u2705 Opened pull request from \`${opts.branch}\`${wikiNote} \xB7 [View pull request](${outcome.pr.url})`;
|
|
952
|
+
\u2705 Opened pull request from \`${opts.branch}\`${wikiNote} \xB7 [View pull request](${outcome.pr.url})${mergeNote}`;
|
|
816
953
|
case "pushed":
|
|
817
954
|
return `
|
|
818
955
|
|
|
819
956
|
---
|
|
820
|
-
\u26A0\uFE0F Pushed \`${opts.branch}\`${wikiNote}, but couldn't open a pull request (no diff against the base branch, or one is already open)
|
|
957
|
+
\u26A0\uFE0F Pushed \`${opts.branch}\`${wikiNote}, but couldn't open a pull request (no diff against the base branch, or one is already open).${mergeNote}`;
|
|
821
958
|
case "none":
|
|
822
959
|
return `
|
|
823
960
|
|
|
@@ -825,51 +962,67 @@ function outcomeBanner(outcome, opts) {
|
|
|
825
962
|
\u2139\uFE0F **No code changes were made** \u2014 ${opts.noChange ?? "there was nothing to open a pull request for."}`;
|
|
826
963
|
}
|
|
827
964
|
}
|
|
828
|
-
async function processJob(ctx) {
|
|
965
|
+
async function processJob(ctx, abort = new AbortController()) {
|
|
829
966
|
const { dir, reused } = await acquireWorkspace(ctx.workspaceKey);
|
|
830
967
|
let prepared = false;
|
|
831
968
|
try {
|
|
832
969
|
if (ctx.kind === "init") {
|
|
833
970
|
await prepareAtSha(ctx, dir, reused);
|
|
834
971
|
prepared = true;
|
|
835
|
-
return await processInitJob(ctx, dir);
|
|
972
|
+
return await processInitJob(ctx, dir, abort);
|
|
836
973
|
}
|
|
837
974
|
if (ctx.kind === "implement") {
|
|
838
975
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
839
976
|
prepared = true;
|
|
840
|
-
return await processImplementJob(ctx, dir, resumed);
|
|
977
|
+
return await processImplementJob(ctx, dir, resumed, abort);
|
|
841
978
|
}
|
|
842
979
|
if (ctx.kind === "revise") {
|
|
843
980
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
844
981
|
prepared = true;
|
|
845
|
-
return await processReviseJob(ctx, dir, resumed);
|
|
982
|
+
return await processReviseJob(ctx, dir, resumed, abort);
|
|
983
|
+
}
|
|
984
|
+
if (ctx.kind === "resolve") {
|
|
985
|
+
await prepareResumingBranch(ctx, dir, reused);
|
|
986
|
+
prepared = true;
|
|
987
|
+
return await processResolveJob(ctx, dir, abort);
|
|
988
|
+
}
|
|
989
|
+
if (ctx.kind === "release") {
|
|
990
|
+
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
991
|
+
prepared = true;
|
|
992
|
+
return await processReleaseJob(ctx, dir, resumed, abort);
|
|
846
993
|
}
|
|
847
994
|
await prepareAtSha(ctx, dir, reused);
|
|
848
995
|
prepared = true;
|
|
849
|
-
return await processChatJob(ctx, dir);
|
|
996
|
+
return await processChatJob(ctx, dir, abort);
|
|
850
997
|
} catch (err) {
|
|
851
|
-
if (
|
|
998
|
+
if (abort.signal.aborted && prepared) {
|
|
999
|
+
await resetWorkspace(dir);
|
|
1000
|
+
} else if (!prepared) {
|
|
1001
|
+
await discardWorkspace(ctx.workspaceKey);
|
|
1002
|
+
}
|
|
852
1003
|
throw err;
|
|
853
1004
|
}
|
|
854
1005
|
}
|
|
855
|
-
async function processInitJob(ctx, dir) {
|
|
1006
|
+
async function processInitJob(ctx, dir, abort) {
|
|
856
1007
|
console.log(`
|
|
857
1008
|
\u25B6 Init ${ctx.jobId} \u2014 ${ctx.repo.fullName} on ${ctx.repo.checkoutBranch}`);
|
|
858
1009
|
const summary = (await runClaudeCode({
|
|
859
1010
|
cwd: dir,
|
|
860
1011
|
prompt: buildInitPrompt(ctx),
|
|
861
1012
|
permissionMode: ctx.permissionMode,
|
|
862
|
-
maxTurns: INIT_MAX_TURNS
|
|
1013
|
+
maxTurns: INIT_MAX_TURNS,
|
|
1014
|
+
abortController: abort
|
|
863
1015
|
})).text.trim();
|
|
864
1016
|
let reply = summary || "(the agent produced no summary)";
|
|
865
|
-
const outcome = await pushAndOpenPr(ctx, dir);
|
|
1017
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort);
|
|
866
1018
|
reply += outcomeBanner(outcome, {
|
|
867
1019
|
branch: ctx.repo.checkoutBranch,
|
|
868
|
-
noChange: "no files were generated; the wiki may already exist."
|
|
1020
|
+
noChange: "no files were generated; the wiki may already exist.",
|
|
1021
|
+
autoMerged
|
|
869
1022
|
});
|
|
870
1023
|
return { text: reply, widgets: [] };
|
|
871
1024
|
}
|
|
872
|
-
async function processChatJob(ctx, dir) {
|
|
1025
|
+
async function processChatJob(ctx, dir, abort) {
|
|
873
1026
|
console.log(`
|
|
874
1027
|
\u25B6 Job ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
875
1028
|
const orchestrating = ctx.permissionMode !== "plan";
|
|
@@ -878,6 +1031,7 @@ async function processChatJob(ctx, dir) {
|
|
|
878
1031
|
cwd: dir,
|
|
879
1032
|
prompt: buildPrompt(ctx),
|
|
880
1033
|
permissionMode: ctx.permissionMode,
|
|
1034
|
+
abortController: abort,
|
|
881
1035
|
...orchestrating ? { model: ORCHESTRATOR_MODEL, maxTurns: ORCHESTRATOR_MAX_TURNS } : {}
|
|
882
1036
|
});
|
|
883
1037
|
const summary = result.text.trim();
|
|
@@ -902,18 +1056,19 @@ async function processChatJob(ctx, dir) {
|
|
|
902
1056
|
cwd: dir,
|
|
903
1057
|
prompt: buildDocumentPrompt(ctx),
|
|
904
1058
|
permissionMode: ctx.permissionMode,
|
|
905
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
1059
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
1060
|
+
abortController: abort
|
|
906
1061
|
});
|
|
907
1062
|
documented = true;
|
|
908
1063
|
} catch (err) {
|
|
909
1064
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
910
1065
|
}
|
|
911
1066
|
}
|
|
912
|
-
const outcome = await pushAndOpenPr(ctx, dir);
|
|
913
|
-
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
1067
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort);
|
|
1068
|
+
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
914
1069
|
return { text: reply, widgets: [] };
|
|
915
1070
|
}
|
|
916
|
-
async function processImplementJob(ctx, dir, resumed) {
|
|
1071
|
+
async function processImplementJob(ctx, dir, resumed, abort) {
|
|
917
1072
|
console.log(`
|
|
918
1073
|
\u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
919
1074
|
const installResult = await installDependencies(dir);
|
|
@@ -922,7 +1077,8 @@ async function processImplementJob(ctx, dir, resumed) {
|
|
|
922
1077
|
prompt: buildPrompt(ctx),
|
|
923
1078
|
permissionMode: ctx.permissionMode,
|
|
924
1079
|
model: ORCHESTRATOR_MODEL,
|
|
925
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
1080
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1081
|
+
abortController: abort
|
|
926
1082
|
});
|
|
927
1083
|
let reply = result.text.trim() || "(the agent produced no report)";
|
|
928
1084
|
if (installResult.status === "failed") {
|
|
@@ -938,18 +1094,19 @@ async function processImplementJob(ctx, dir, resumed) {
|
|
|
938
1094
|
cwd: dir,
|
|
939
1095
|
prompt: buildDocumentPrompt(ctx),
|
|
940
1096
|
permissionMode: ctx.permissionMode,
|
|
941
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
1097
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
1098
|
+
abortController: abort
|
|
942
1099
|
});
|
|
943
1100
|
documented = true;
|
|
944
1101
|
} catch (err) {
|
|
945
1102
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
946
1103
|
}
|
|
947
1104
|
}
|
|
948
|
-
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
949
|
-
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
1105
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
|
|
1106
|
+
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
950
1107
|
return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
|
|
951
1108
|
}
|
|
952
|
-
async function processReviseJob(ctx, dir, resumed) {
|
|
1109
|
+
async function processReviseJob(ctx, dir, resumed, abort) {
|
|
953
1110
|
console.log(`
|
|
954
1111
|
\u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
955
1112
|
const installResult = await installDependencies(dir);
|
|
@@ -958,7 +1115,8 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
958
1115
|
prompt: buildRevisePrompt(ctx),
|
|
959
1116
|
permissionMode: ctx.permissionMode,
|
|
960
1117
|
model: ORCHESTRATOR_MODEL,
|
|
961
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
1118
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1119
|
+
abortController: abort
|
|
962
1120
|
});
|
|
963
1121
|
const summary = result.text.trim();
|
|
964
1122
|
let reply = summary || "(the agent produced no reply)";
|
|
@@ -980,16 +1138,17 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
980
1138
|
cwd: dir,
|
|
981
1139
|
prompt: buildDocumentPrompt(ctx),
|
|
982
1140
|
permissionMode: ctx.permissionMode,
|
|
983
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
1141
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
1142
|
+
abortController: abort
|
|
984
1143
|
});
|
|
985
1144
|
documented = true;
|
|
986
1145
|
} catch (err) {
|
|
987
1146
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
988
1147
|
}
|
|
989
1148
|
}
|
|
990
|
-
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
1149
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
|
|
991
1150
|
if (outcome.kind !== "none") {
|
|
992
|
-
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
1151
|
+
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
993
1152
|
}
|
|
994
1153
|
return {
|
|
995
1154
|
text: reply,
|
|
@@ -998,6 +1157,59 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
998
1157
|
...result.plans?.length ? { plans: result.plans } : {}
|
|
999
1158
|
};
|
|
1000
1159
|
}
|
|
1160
|
+
async function processResolveJob(ctx, dir, abort) {
|
|
1161
|
+
console.log(`
|
|
1162
|
+
\u25B6 Resolve ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1163
|
+
const installResult = await installDependencies(dir);
|
|
1164
|
+
const { resolved, text } = await mergeAndResolveConflicts(ctx, dir, abort);
|
|
1165
|
+
let reply = resolved ? text || "(the agent produced no report)" : `Merged \`${ctx.repo.mergeBranch ?? "the merge branch"}\` into \`${ctx.repo.checkoutBranch}\` cleanly \u2014 there were no conflicts to resolve.`;
|
|
1166
|
+
if (installResult.status === "failed") {
|
|
1167
|
+
reply += `
|
|
1168
|
+
|
|
1169
|
+
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
1170
|
+
}
|
|
1171
|
+
if (abort.signal.aborted) throw new Error("Run canceled by user");
|
|
1172
|
+
await commitWithRepair(ctx, dir, abort);
|
|
1173
|
+
await pushBranch(ctx, dir);
|
|
1174
|
+
const pr = await openPullRequest(ctx);
|
|
1175
|
+
const outcome = pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
1176
|
+
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch });
|
|
1177
|
+
return { text: reply, widgets: [], ...pr ? { pr } : {} };
|
|
1178
|
+
}
|
|
1179
|
+
async function processReleaseJob(ctx, dir, resumed, abort) {
|
|
1180
|
+
console.log(`
|
|
1181
|
+
\u25B6 Release ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1182
|
+
const installResult = await installDependencies(dir);
|
|
1183
|
+
const result = await runClaudeCode({
|
|
1184
|
+
cwd: dir,
|
|
1185
|
+
prompt: buildReleasePrompt(ctx),
|
|
1186
|
+
permissionMode: ctx.permissionMode,
|
|
1187
|
+
model: ORCHESTRATOR_MODEL,
|
|
1188
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1189
|
+
abortController: abort
|
|
1190
|
+
});
|
|
1191
|
+
let reply = result.text.trim() || "(the agent produced no reply)";
|
|
1192
|
+
if (installResult.status === "failed") {
|
|
1193
|
+
reply += `
|
|
1194
|
+
|
|
1195
|
+
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
1196
|
+
}
|
|
1197
|
+
if (result.widgets.length > 0) {
|
|
1198
|
+
console.log(
|
|
1199
|
+
` \u2026release ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`
|
|
1200
|
+
);
|
|
1201
|
+
return { text: reply, widgets: result.widgets };
|
|
1202
|
+
}
|
|
1203
|
+
const { outcome, autoMerged } = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
|
|
1204
|
+
if (outcome.kind !== "none") {
|
|
1205
|
+
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, autoMerged });
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
text: reply,
|
|
1209
|
+
widgets: [],
|
|
1210
|
+
...outcome.kind === "pr" ? { pr: outcome.pr } : {}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1001
1213
|
async function heartbeat(config) {
|
|
1002
1214
|
const health = await checkClaudeCode();
|
|
1003
1215
|
if (health.ready) {
|
|
@@ -1016,43 +1228,78 @@ async function pollLoop(config) {
|
|
|
1016
1228
|
const swept = await sweepWorkspaces();
|
|
1017
1229
|
if (swept > 0) console.log(`Cleared ${swept} stale workspace${swept === 1 ? "" : "s"}.`);
|
|
1018
1230
|
await heartbeat(config);
|
|
1019
|
-
let
|
|
1020
|
-
|
|
1021
|
-
|
|
1231
|
+
let heartbeatTimer;
|
|
1232
|
+
const scheduleHeartbeat = () => {
|
|
1233
|
+
heartbeatTimer = setTimeout(async () => {
|
|
1022
1234
|
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}`);
|
|
1235
|
+
scheduleHeartbeat();
|
|
1236
|
+
}, HEARTBEAT_MS);
|
|
1237
|
+
};
|
|
1238
|
+
scheduleHeartbeat();
|
|
1239
|
+
try {
|
|
1240
|
+
for (; ; ) {
|
|
1241
|
+
let ctx = null;
|
|
1050
1242
|
try {
|
|
1051
|
-
await
|
|
1052
|
-
} catch (
|
|
1053
|
-
console.error(`
|
|
1243
|
+
ctx = await claimJob(config);
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
console.error(`Claim error: ${errorMessage2(err)}`);
|
|
1246
|
+
await sleep(IDLE_MS);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (!ctx) {
|
|
1250
|
+
await sleep(IDLE_MS);
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const abort = new AbortController();
|
|
1254
|
+
let stopPolling = false;
|
|
1255
|
+
const scheduleCancelPoll = () => {
|
|
1256
|
+
setTimeout(async () => {
|
|
1257
|
+
if (stopPolling) return;
|
|
1258
|
+
try {
|
|
1259
|
+
if (await pollJobCanceling(config, ctx.jobId)) {
|
|
1260
|
+
console.log(`\u23F9 Stop requested for job ${ctx.jobId} \u2014 aborting\u2026`);
|
|
1261
|
+
abort.abort();
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
if (!stopPolling) scheduleCancelPoll();
|
|
1267
|
+
}, CANCEL_POLL_MS);
|
|
1268
|
+
};
|
|
1269
|
+
scheduleCancelPoll();
|
|
1270
|
+
try {
|
|
1271
|
+
const { text, widgets, pr, plans } = await processJob(ctx, abort);
|
|
1272
|
+
await reportJob(config, ctx.jobId, {
|
|
1273
|
+
status: "done",
|
|
1274
|
+
text,
|
|
1275
|
+
widgets,
|
|
1276
|
+
pr,
|
|
1277
|
+
...plans?.length ? { plans } : {}
|
|
1278
|
+
});
|
|
1279
|
+
console.log(`\u2713 Job ${ctx.jobId} done`);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
if (abort.signal.aborted) {
|
|
1282
|
+
console.log(`\u23F9 Job ${ctx.jobId} canceled`);
|
|
1283
|
+
try {
|
|
1284
|
+
await reportJob(config, ctx.jobId, { status: "canceled" });
|
|
1285
|
+
} catch (reportErr) {
|
|
1286
|
+
console.error(` (failed to report the cancellation: ${errorMessage2(reportErr)})`);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
const message = errorMessage2(err);
|
|
1290
|
+
console.error(`\u2717 Job ${ctx.jobId} failed: ${message}`);
|
|
1291
|
+
try {
|
|
1292
|
+
await reportJob(config, ctx.jobId, { status: "error", error: message });
|
|
1293
|
+
} catch (reportErr) {
|
|
1294
|
+
console.error(` (also failed to report the error: ${errorMessage2(reportErr)})`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
} finally {
|
|
1298
|
+
stopPolling = true;
|
|
1054
1299
|
}
|
|
1055
1300
|
}
|
|
1301
|
+
} finally {
|
|
1302
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
1056
1303
|
}
|
|
1057
1304
|
}
|
|
1058
1305
|
function sleep(ms) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-release
|
|
3
|
+
description: >-
|
|
4
|
+
Draft release notes and version suggestions for a release, then (after the
|
|
5
|
+
user confirms) open a bump PR that updates package.json version(s) and
|
|
6
|
+
CHANGELOG.md. Two-turn flow: first turn asks the user to confirm versions via
|
|
7
|
+
widgets; second turn (answers in thread) writes the bumps and opens the PR.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# create-release
|
|
11
|
+
|
|
12
|
+
You are driving a FlumeCode release. This skill has two distinct phases; detect
|
|
13
|
+
which one applies before acting.
|
|
14
|
+
|
|
15
|
+
## Phase detection
|
|
16
|
+
|
|
17
|
+
Check the thread (`# Conversation so far` in the prompt). If **no widget answers**
|
|
18
|
+
appear in any prior agent turn, this is **Phase 1** — propose versions and ask.
|
|
19
|
+
If a prior turn contains widget-answer selections (the user picked a version), this
|
|
20
|
+
is **Phase 2** — apply the bumps and report.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Phase 1 — Propose versions and ask
|
|
25
|
+
|
|
26
|
+
### 1. Find the last release tags
|
|
27
|
+
|
|
28
|
+
For `@flumecode/web`, find the most recent `v*` tag:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
git tag -l --sort=-version:refname 'v*' | head -1
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For `@flumecode/runner`, find the most recent `runner-v*` tag:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
git tag -l --sort=-version:refname 'runner-v*' | head -1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If no matching tag exists for a package, treat the current version in that
|
|
41
|
+
package's `package.json` as the baseline.
|
|
42
|
+
|
|
43
|
+
### 2. List commits since the last tag, split by package
|
|
44
|
+
|
|
45
|
+
Get all commits since the tag (or all commits if no tag):
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
git log <lastWebTag>..HEAD --oneline
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then split by what they touch:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
# web + shared packages
|
|
55
|
+
git log <lastWebTag>..HEAD --oneline -- apps/web/ packages/
|
|
56
|
+
|
|
57
|
+
# runner only
|
|
58
|
+
git log <lastRunnerTag>..HEAD --oneline -- apps/runner/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If a package has no commits, mark it **unchanged** — do not bump it.
|
|
62
|
+
|
|
63
|
+
### 3. Infer bump level per package
|
|
64
|
+
|
|
65
|
+
For each package that has commits:
|
|
66
|
+
|
|
67
|
+
- `feat:` in a commit subject, or any breaking change → **minor** (or major if
|
|
68
|
+
explicitly breaking).
|
|
69
|
+
- `fix:`, `chore:`, `docs:`, `refactor:`, `test:` → **patch**.
|
|
70
|
+
- When in doubt, default to **patch**.
|
|
71
|
+
|
|
72
|
+
### 4. Compute suggested next versions
|
|
73
|
+
|
|
74
|
+
Read current versions from `apps/web/package.json` and `apps/runner/package.json`.
|
|
75
|
+
Apply the inferred bump (semver: patch = last digit +1, minor = middle digit +1 +
|
|
76
|
+
reset patch to 0). For unchanged packages, keep the current version.
|
|
77
|
+
|
|
78
|
+
### 5. Draft concise release notes
|
|
79
|
+
|
|
80
|
+
3–10 user-readable bullet points summarising what changed. Group by theme; skip
|
|
81
|
+
internal chore commits unless they affect the user.
|
|
82
|
+
|
|
83
|
+
### 6. Ask the user to confirm
|
|
84
|
+
|
|
85
|
+
For **each package that has commits** (skip unchanged ones), call
|
|
86
|
+
`mcp__flume_widgets__single_select` with:
|
|
87
|
+
|
|
88
|
+
- `question`: e.g. `@flumecode/web version for this release?`
|
|
89
|
+
- `options`: the suggested next version, plus the two sibling semver levels (e.g.
|
|
90
|
+
if patch suggested: also offer minor and major), plus the current version as
|
|
91
|
+
"No change (keep X.Y.Z)".
|
|
92
|
+
|
|
93
|
+
Also present the drafted release notes in your reply text so the user can read
|
|
94
|
+
them, then call `mcp__flume_widgets__single_select` for a confirmatory question:
|
|
95
|
+
`Do the release notes look correct?` with options `Yes, use these notes` and
|
|
96
|
+
`I'll edit them in the PR`.
|
|
97
|
+
|
|
98
|
+
**After calling widgets, end your turn.** Do NOT open a PR in Phase 1.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Phase 2 — Apply the bumps and report
|
|
103
|
+
|
|
104
|
+
### 1. Read the widget answers
|
|
105
|
+
|
|
106
|
+
The user's confirmed version selections appear in the `# Conversation so far`
|
|
107
|
+
thread as agent messages (the widget-answer turn). Extract the chosen version for
|
|
108
|
+
each package from those selections.
|
|
109
|
+
|
|
110
|
+
### 2. Update package.json files
|
|
111
|
+
|
|
112
|
+
For each package whose version changed, edit the `"version"` field in:
|
|
113
|
+
|
|
114
|
+
- `apps/web/package.json` — for `@flumecode/web`
|
|
115
|
+
- `apps/runner/package.json` — for `@flumecode/runner`
|
|
116
|
+
|
|
117
|
+
Change only the `"version"` line; do not reformat the file.
|
|
118
|
+
|
|
119
|
+
### 3. Update CHANGELOG.md
|
|
120
|
+
|
|
121
|
+
Edit (or create) `CHANGELOG.md` at the repo root. Insert a new section at the
|
|
122
|
+
top, below any existing `# Changelog` title line:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
## [X.Y.Z / runner-X.Y.Z] - YYYY-MM-DD
|
|
126
|
+
|
|
127
|
+
- Bullet point from release notes
|
|
128
|
+
- Another bullet point
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use the ISO date format (`YYYY-MM-DD`). Preserve all existing entries — do not
|
|
132
|
+
delete or rewrite prior sections.
|
|
133
|
+
|
|
134
|
+
If both packages are bumped, list both versions in the heading (e.g.
|
|
135
|
+
`## [0.9.0 / runner-0.5.0] - 2026-06-06`). If only one package is bumped, list
|
|
136
|
+
only that version in the heading.
|
|
137
|
+
|
|
138
|
+
### 4. Stop — do not commit or push
|
|
139
|
+
|
|
140
|
+
Leave the edited files in the working tree. The runner commits them and opens the
|
|
141
|
+
pull request.
|
|
142
|
+
|
|
143
|
+
### 5. End with this exact report format
|
|
144
|
+
|
|
145
|
+
Your final message must match this shape (adjust versions and files to match what
|
|
146
|
+
actually changed):
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
**Bumped versions:**
|
|
150
|
+
- `@flumecode/web`: `0.8.0` → `0.9.0`
|
|
151
|
+
- `@flumecode/runner`: `0.4.0` → `0.5.0`
|
|
152
|
+
|
|
153
|
+
**CHANGELOG updated** with release notes.
|
|
154
|
+
|
|
155
|
+
**Files changed:** `apps/web/package.json`, `apps/runner/package.json`, `CHANGELOG.md`
|
|
156
|
+
|
|
157
|
+
<!-- flumecode:versions {"@flumecode/web":"0.9.0","@flumecode/runner":"0.5.0"} -->
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The `<!-- flumecode:versions {...} -->` comment is machine-readable and **must be
|
|
161
|
+
the last line** of your message body (before any trailing newline). The runner
|
|
162
|
+
reads it to persist the confirmed versions on the release entity. Use the exact
|
|
163
|
+
JSON key names `@flumecode/web` and `@flumecode/runner`; omit a package if its
|
|
164
|
+
version did not change.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Notes
|
|
169
|
+
|
|
170
|
+
- **Runner-only bump:** if only `apps/runner/` has commits, bump only
|
|
171
|
+
`apps/runner/package.json`. Leave `apps/web/package.json` unchanged.
|
|
172
|
+
- **Clear Phase 1 text:** be explicit about what changed since the last tag so the
|
|
173
|
+
user can confidently confirm or override your suggestions.
|
|
174
|
+
- **Never edit** any file other than `apps/web/package.json`,
|
|
175
|
+
`apps/runner/package.json`, and `CHANGELOG.md`.
|
|
176
|
+
- **Never commit, push, or open a PR** — the runner does that.
|
|
@@ -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.
|