@flumecode/runner 0.3.1 → 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
|
@@ -40,6 +40,7 @@ function readVersion() {
|
|
|
40
40
|
var RUNNER_VERSION = readVersion();
|
|
41
41
|
var RUNNER_VERSION_HEADER = "x-flumecode-runner-version";
|
|
42
42
|
var RUNNER_MIN_VERSION_HEADER = "x-flumecode-min-runner-version";
|
|
43
|
+
var RUNNER_LATEST_VERSION_HEADER = "x-flumecode-latest-runner-version";
|
|
43
44
|
function compareVersions(a, b) {
|
|
44
45
|
const parse = (v) => (v.split("-")[0] ?? v).split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
45
46
|
const pa = parse(a);
|
|
@@ -55,16 +56,29 @@ function compareVersions(a, b) {
|
|
|
55
56
|
// src/api.ts
|
|
56
57
|
var OUTDATED_WARN_INTERVAL_MS = 60 * 6e4;
|
|
57
58
|
var lastOutdatedWarnAt = 0;
|
|
59
|
+
var UPDATE_NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
60
|
+
var lastUpdateNudgeAt = 0;
|
|
58
61
|
function noteServerVersion(res) {
|
|
59
62
|
const min = res.headers.get(RUNNER_MIN_VERSION_HEADER)?.trim();
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`\u26A0\uFE0F This runner (v${RUNNER_VERSION}) is outdated \u2014 the server expects v${min} or newer.
|
|
63
|
+
if (min && RUNNER_VERSION !== "unknown" && compareVersions(RUNNER_VERSION, min) < 0) {
|
|
64
|
+
const now2 = Date.now();
|
|
65
|
+
if (now2 - lastOutdatedWarnAt >= OUTDATED_WARN_INTERVAL_MS) {
|
|
66
|
+
lastOutdatedWarnAt = now2;
|
|
67
|
+
console.warn(
|
|
68
|
+
`\u26A0\uFE0F This runner (v${RUNNER_VERSION}) is outdated \u2014 the server expects v${min} or newer.
|
|
67
69
|
Update with: npm install -g @flumecode/runner@latest`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const latest = res.headers.get(RUNNER_LATEST_VERSION_HEADER)?.trim();
|
|
75
|
+
if (!latest || RUNNER_VERSION === "unknown") return;
|
|
76
|
+
if (compareVersions(RUNNER_VERSION, latest) >= 0) return;
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (now - lastUpdateNudgeAt < UPDATE_NUDGE_INTERVAL_MS) return;
|
|
79
|
+
lastUpdateNudgeAt = now;
|
|
80
|
+
console.log(
|
|
81
|
+
`\u2139\uFE0F A newer runner is available (v${latest}; you have v${RUNNER_VERSION}). Update with: npm install -g @flumecode/runner@latest`
|
|
68
82
|
);
|
|
69
83
|
}
|
|
70
84
|
async function claimJob(config) {
|
|
@@ -83,6 +97,19 @@ async function claimJob(config) {
|
|
|
83
97
|
if (!res.ok) throw new Error(`claim failed: ${res.status} ${await safeText(res)}`);
|
|
84
98
|
return await res.json();
|
|
85
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
|
+
}
|
|
86
113
|
async function reportJob(config, jobId, result) {
|
|
87
114
|
const res = await fetch(`${config.serverUrl}/api/runner/jobs/${jobId}/complete`, {
|
|
88
115
|
method: "POST",
|
|
@@ -265,19 +292,30 @@ function renderPlan(plan) {
|
|
|
265
292
|
lines.push(PLAN_MARKER);
|
|
266
293
|
return lines.join("\n");
|
|
267
294
|
}
|
|
295
|
+
var submitPlanInputSchema = {
|
|
296
|
+
plans: z2.array(z2.object(planInputSchema)).min(1).refine(
|
|
297
|
+
(arr) => {
|
|
298
|
+
const titles = arr.map((p) => p.title.trim()).filter((t) => t.length > 0);
|
|
299
|
+
return new Set(titles).size === titles.length;
|
|
300
|
+
},
|
|
301
|
+
{ message: "Each plan must have a distinct non-empty title" }
|
|
302
|
+
)
|
|
303
|
+
};
|
|
304
|
+
var submitPlanSchema = z2.object(submitPlanInputSchema);
|
|
268
305
|
function createPlanTooling() {
|
|
269
|
-
let
|
|
306
|
+
let renderedPlans = null;
|
|
270
307
|
const submitPlan = tool2(
|
|
271
308
|
SUBMIT_PLAN,
|
|
272
|
-
"Submit
|
|
273
|
-
|
|
309
|
+
"Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable Accept-as-plan draft. Do NOT call submit_plan more than once. acceptanceCriteria is required in each plan and must contain at least 2 observable, verifiable conditions. The 'title' field names each specific plan \u2014 make it concise and distinct from the request title and from sibling plan titles.",
|
|
310
|
+
submitPlanInputSchema,
|
|
274
311
|
async (args) => {
|
|
275
|
-
|
|
312
|
+
const parsed = submitPlanSchema.parse(args);
|
|
313
|
+
renderedPlans = parsed.plans.map(renderPlan);
|
|
276
314
|
return {
|
|
277
315
|
content: [
|
|
278
316
|
{
|
|
279
317
|
type: "text",
|
|
280
|
-
text: "Plan submitted. The runner will render and post
|
|
318
|
+
text: "Plan(s) submitted. The runner will render and post them as your comment(s). End your turn now."
|
|
281
319
|
}
|
|
282
320
|
]
|
|
283
321
|
};
|
|
@@ -287,7 +325,7 @@ function createPlanTooling() {
|
|
|
287
325
|
name: SERVER_NAME2,
|
|
288
326
|
tools: [submitPlan]
|
|
289
327
|
});
|
|
290
|
-
return { mcpServer,
|
|
328
|
+
return { mcpServer, getPlans: () => renderedPlans };
|
|
291
329
|
}
|
|
292
330
|
|
|
293
331
|
// src/executor.ts
|
|
@@ -295,7 +333,7 @@ var FLUME_PLUGIN_DIR = fileURLToPath2(new URL("../skills-plugin", import.meta.ur
|
|
|
295
333
|
async function runClaudeCode(opts) {
|
|
296
334
|
let finalText = "";
|
|
297
335
|
const { mcpServer, collected } = createWidgetTooling();
|
|
298
|
-
const { mcpServer: planServer,
|
|
336
|
+
const { mcpServer: planServer, getPlans } = createPlanTooling();
|
|
299
337
|
for await (const message of query({
|
|
300
338
|
prompt: opts.prompt,
|
|
301
339
|
options: {
|
|
@@ -303,6 +341,7 @@ async function runClaudeCode(opts) {
|
|
|
303
341
|
permissionMode: opts.permissionMode,
|
|
304
342
|
allowDangerouslySkipPermissions: opts.permissionMode === "bypassPermissions",
|
|
305
343
|
...opts.model ? { model: opts.model } : {},
|
|
344
|
+
...opts.abortController ? { abortController: opts.abortController } : {},
|
|
306
345
|
maxTurns: opts.maxTurns ?? 40,
|
|
307
346
|
// Hermetic: ignore ALL config from the checked-out repo (CLAUDE.md,
|
|
308
347
|
// .claude/settings.json, .claude/skills, …) and the runner owner's
|
|
@@ -333,7 +372,10 @@ async function runClaudeCode(opts) {
|
|
|
333
372
|
}
|
|
334
373
|
}
|
|
335
374
|
process.stdout.write("\n");
|
|
336
|
-
|
|
375
|
+
if (opts.abortController?.signal.aborted) {
|
|
376
|
+
throw new Error("Run canceled by user");
|
|
377
|
+
}
|
|
378
|
+
return { text: finalText, widgets: collected, plans: getPlans() };
|
|
337
379
|
}
|
|
338
380
|
|
|
339
381
|
// src/health.ts
|
|
@@ -399,6 +441,20 @@ function stripFrontMatter(raw) {
|
|
|
399
441
|
}
|
|
400
442
|
|
|
401
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
|
+
}
|
|
402
458
|
function buildPrompt(ctx) {
|
|
403
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.`;
|
|
404
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.`;
|
|
@@ -422,12 +478,7 @@ function buildPrompt(ctx) {
|
|
|
422
478
|
if (ctx.request?.body) {
|
|
423
479
|
lines.push("", ctx.request.body);
|
|
424
480
|
}
|
|
425
|
-
|
|
426
|
-
lines.push("", "# Conversation so far");
|
|
427
|
-
for (const turn of ctx.thread) {
|
|
428
|
-
lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
481
|
+
appendThread(lines, ctx);
|
|
431
482
|
lines.push(
|
|
432
483
|
"",
|
|
433
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."
|
|
@@ -435,7 +486,7 @@ function buildPrompt(ctx) {
|
|
|
435
486
|
return lines.join("\n");
|
|
436
487
|
}
|
|
437
488
|
function buildRevisePrompt(ctx) {
|
|
438
|
-
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.`;
|
|
439
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.`;
|
|
440
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.`;
|
|
441
492
|
const lines = [
|
|
@@ -454,21 +505,39 @@ function buildRevisePrompt(ctx) {
|
|
|
454
505
|
if (ctx.request?.body) {
|
|
455
506
|
lines.push("", ctx.request.body);
|
|
456
507
|
}
|
|
457
|
-
|
|
458
|
-
lines.push("", "# Latest implementation report", "", ctx.priorReport);
|
|
459
|
-
}
|
|
460
|
-
if (ctx.thread && ctx.thread.length > 0) {
|
|
461
|
-
lines.push("", "# Conversation so far");
|
|
462
|
-
for (const turn of ctx.thread) {
|
|
463
|
-
lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
508
|
+
appendThread(lines, ctx);
|
|
466
509
|
lines.push(
|
|
467
510
|
"",
|
|
468
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."
|
|
469
512
|
);
|
|
470
513
|
return lines.join("\n");
|
|
471
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
|
+
}
|
|
472
541
|
function buildDocumentPrompt(ctx) {
|
|
473
542
|
const lines = [
|
|
474
543
|
`You are "${ctx.agentName}" maintaining the repository wiki for ${ctx.repo.fullName}.`,
|
|
@@ -480,12 +549,7 @@ function buildDocumentPrompt(ctx) {
|
|
|
480
549
|
if (ctx.request?.body) {
|
|
481
550
|
lines.push("", ctx.request.body);
|
|
482
551
|
}
|
|
483
|
-
|
|
484
|
-
lines.push("", "# Conversation so far");
|
|
485
|
-
for (const turn of ctx.thread) {
|
|
486
|
-
lines.push("", `## ${turn.role === "agent" ? ctx.agentName : "User"}`, turn.content);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
552
|
+
appendThread(lines, ctx);
|
|
489
553
|
lines.push("", "When done, reply with a one- or two-line summary of the wiki changes you made.");
|
|
490
554
|
return lines.join("\n");
|
|
491
555
|
}
|
|
@@ -589,6 +653,12 @@ async function discardWorkspace(key) {
|
|
|
589
653
|
});
|
|
590
654
|
}
|
|
591
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
|
+
}
|
|
592
662
|
async function prepareAtSha(ctx, dir, reused) {
|
|
593
663
|
if (!reused) {
|
|
594
664
|
await cloneAtSha(ctx, dir);
|
|
@@ -702,6 +772,33 @@ async function rebaseOntoMergeBranch(ctx, dir) {
|
|
|
702
772
|
throw new RebaseConflictError(mergeBranch, files);
|
|
703
773
|
}
|
|
704
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
|
+
}
|
|
705
802
|
async function openPullRequest(ctx) {
|
|
706
803
|
const { owner, name, cloneToken, checkoutBranch, mergeBranch } = ctx.repo;
|
|
707
804
|
if (!mergeBranch) return null;
|
|
@@ -747,21 +844,23 @@ async function cleanup(dir) {
|
|
|
747
844
|
|
|
748
845
|
// src/run.ts
|
|
749
846
|
var IDLE_MS = 5e3;
|
|
847
|
+
var CANCEL_POLL_MS = 2500;
|
|
750
848
|
var ORCHESTRATOR_MODEL = "sonnet";
|
|
751
849
|
var ORCHESTRATOR_MAX_TURNS = 80;
|
|
752
850
|
var MAX_COMMIT_REPAIRS = 2;
|
|
753
851
|
var INIT_MAX_TURNS = 200;
|
|
754
852
|
var DOCUMENT_MAX_TURNS = 120;
|
|
755
853
|
var HEARTBEAT_MS = 5 * 6e4;
|
|
756
|
-
async function pushAndOpenPr(ctx, dir, opts = { rebase: true }) {
|
|
757
|
-
|
|
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);
|
|
758
857
|
if (!committed) return { kind: "none" };
|
|
759
858
|
if (opts.rebase) await rebaseOntoMergeBranch(ctx, dir);
|
|
760
859
|
await pushBranch(ctx, dir);
|
|
761
860
|
const pr = await openPullRequest(ctx);
|
|
762
861
|
return pr ? { kind: "pr", pr } : { kind: "pushed" };
|
|
763
862
|
}
|
|
764
|
-
async function commitWithRepair(ctx, dir) {
|
|
863
|
+
async function commitWithRepair(ctx, dir, abort) {
|
|
765
864
|
for (let attempt = 1; ; attempt++) {
|
|
766
865
|
try {
|
|
767
866
|
return await commitChanges(ctx, dir);
|
|
@@ -775,7 +874,8 @@ async function commitWithRepair(ctx, dir) {
|
|
|
775
874
|
prompt: buildRepairPrompt(ctx, err.log),
|
|
776
875
|
permissionMode: ctx.permissionMode,
|
|
777
876
|
model: ORCHESTRATOR_MODEL,
|
|
778
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
877
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
878
|
+
abortController: abort
|
|
779
879
|
});
|
|
780
880
|
}
|
|
781
881
|
}
|
|
@@ -800,51 +900,61 @@ function outcomeBanner(outcome, opts) {
|
|
|
800
900
|
\u2139\uFE0F **No code changes were made** \u2014 ${opts.noChange ?? "there was nothing to open a pull request for."}`;
|
|
801
901
|
}
|
|
802
902
|
}
|
|
803
|
-
async function processJob(ctx) {
|
|
903
|
+
async function processJob(ctx, abort = new AbortController()) {
|
|
804
904
|
const { dir, reused } = await acquireWorkspace(ctx.workspaceKey);
|
|
805
905
|
let prepared = false;
|
|
806
906
|
try {
|
|
807
907
|
if (ctx.kind === "init") {
|
|
808
908
|
await prepareAtSha(ctx, dir, reused);
|
|
809
909
|
prepared = true;
|
|
810
|
-
return await processInitJob(ctx, dir);
|
|
910
|
+
return await processInitJob(ctx, dir, abort);
|
|
811
911
|
}
|
|
812
912
|
if (ctx.kind === "implement") {
|
|
813
913
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
814
914
|
prepared = true;
|
|
815
|
-
return await processImplementJob(ctx, dir, resumed);
|
|
915
|
+
return await processImplementJob(ctx, dir, resumed, abort);
|
|
816
916
|
}
|
|
817
917
|
if (ctx.kind === "revise") {
|
|
818
918
|
const { resumed } = await prepareResumingBranch(ctx, dir, reused);
|
|
819
919
|
prepared = true;
|
|
820
|
-
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);
|
|
821
926
|
}
|
|
822
927
|
await prepareAtSha(ctx, dir, reused);
|
|
823
928
|
prepared = true;
|
|
824
|
-
return await processChatJob(ctx, dir);
|
|
929
|
+
return await processChatJob(ctx, dir, abort);
|
|
825
930
|
} catch (err) {
|
|
826
|
-
if (
|
|
931
|
+
if (abort.signal.aborted && prepared) {
|
|
932
|
+
await resetWorkspace(dir);
|
|
933
|
+
} else if (!prepared) {
|
|
934
|
+
await discardWorkspace(ctx.workspaceKey);
|
|
935
|
+
}
|
|
827
936
|
throw err;
|
|
828
937
|
}
|
|
829
938
|
}
|
|
830
|
-
async function processInitJob(ctx, dir) {
|
|
939
|
+
async function processInitJob(ctx, dir, abort) {
|
|
831
940
|
console.log(`
|
|
832
941
|
\u25B6 Init ${ctx.jobId} \u2014 ${ctx.repo.fullName} on ${ctx.repo.checkoutBranch}`);
|
|
833
942
|
const summary = (await runClaudeCode({
|
|
834
943
|
cwd: dir,
|
|
835
944
|
prompt: buildInitPrompt(ctx),
|
|
836
945
|
permissionMode: ctx.permissionMode,
|
|
837
|
-
maxTurns: INIT_MAX_TURNS
|
|
946
|
+
maxTurns: INIT_MAX_TURNS,
|
|
947
|
+
abortController: abort
|
|
838
948
|
})).text.trim();
|
|
839
949
|
let reply = summary || "(the agent produced no summary)";
|
|
840
|
-
const outcome = await pushAndOpenPr(ctx, dir);
|
|
950
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort);
|
|
841
951
|
reply += outcomeBanner(outcome, {
|
|
842
952
|
branch: ctx.repo.checkoutBranch,
|
|
843
953
|
noChange: "no files were generated; the wiki may already exist."
|
|
844
954
|
});
|
|
845
955
|
return { text: reply, widgets: [] };
|
|
846
956
|
}
|
|
847
|
-
async function processChatJob(ctx, dir) {
|
|
957
|
+
async function processChatJob(ctx, dir, abort) {
|
|
848
958
|
console.log(`
|
|
849
959
|
\u25B6 Job ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
850
960
|
const orchestrating = ctx.permissionMode !== "plan";
|
|
@@ -853,18 +963,19 @@ async function processChatJob(ctx, dir) {
|
|
|
853
963
|
cwd: dir,
|
|
854
964
|
prompt: buildPrompt(ctx),
|
|
855
965
|
permissionMode: ctx.permissionMode,
|
|
966
|
+
abortController: abort,
|
|
856
967
|
...orchestrating ? { model: ORCHESTRATOR_MODEL, maxTurns: ORCHESTRATOR_MAX_TURNS } : {}
|
|
857
968
|
});
|
|
858
969
|
const summary = result.text.trim();
|
|
859
970
|
let reply = summary || "(the agent produced no summary)";
|
|
860
|
-
if (result.plan) {
|
|
861
|
-
reply = result.plan;
|
|
862
|
-
}
|
|
863
971
|
if (installResult?.status === "failed") {
|
|
864
972
|
reply += `
|
|
865
973
|
|
|
866
974
|
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
867
975
|
}
|
|
976
|
+
if (result.plans?.length) {
|
|
977
|
+
return { text: result.text.trim(), widgets: [], plans: result.plans };
|
|
978
|
+
}
|
|
868
979
|
if (result.widgets.length > 0) {
|
|
869
980
|
console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
|
|
870
981
|
return { text: reply, widgets: result.widgets };
|
|
@@ -877,18 +988,19 @@ async function processChatJob(ctx, dir) {
|
|
|
877
988
|
cwd: dir,
|
|
878
989
|
prompt: buildDocumentPrompt(ctx),
|
|
879
990
|
permissionMode: ctx.permissionMode,
|
|
880
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
991
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
992
|
+
abortController: abort
|
|
881
993
|
});
|
|
882
994
|
documented = true;
|
|
883
995
|
} catch (err) {
|
|
884
996
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
885
997
|
}
|
|
886
998
|
}
|
|
887
|
-
const outcome = await pushAndOpenPr(ctx, dir);
|
|
999
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort);
|
|
888
1000
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
889
1001
|
return { text: reply, widgets: [] };
|
|
890
1002
|
}
|
|
891
|
-
async function processImplementJob(ctx, dir, resumed) {
|
|
1003
|
+
async function processImplementJob(ctx, dir, resumed, abort) {
|
|
892
1004
|
console.log(`
|
|
893
1005
|
\u25B6 Implement ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
894
1006
|
const installResult = await installDependencies(dir);
|
|
@@ -897,7 +1009,8 @@ async function processImplementJob(ctx, dir, resumed) {
|
|
|
897
1009
|
prompt: buildPrompt(ctx),
|
|
898
1010
|
permissionMode: ctx.permissionMode,
|
|
899
1011
|
model: ORCHESTRATOR_MODEL,
|
|
900
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
1012
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1013
|
+
abortController: abort
|
|
901
1014
|
});
|
|
902
1015
|
let reply = result.text.trim() || "(the agent produced no report)";
|
|
903
1016
|
if (installResult.status === "failed") {
|
|
@@ -913,18 +1026,19 @@ async function processImplementJob(ctx, dir, resumed) {
|
|
|
913
1026
|
cwd: dir,
|
|
914
1027
|
prompt: buildDocumentPrompt(ctx),
|
|
915
1028
|
permissionMode: ctx.permissionMode,
|
|
916
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
1029
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
1030
|
+
abortController: abort
|
|
917
1031
|
});
|
|
918
1032
|
documented = true;
|
|
919
1033
|
} catch (err) {
|
|
920
1034
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
921
1035
|
}
|
|
922
1036
|
}
|
|
923
|
-
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
1037
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
|
|
924
1038
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
925
1039
|
return { text: reply, widgets: [], ...outcome.kind === "pr" ? { pr: outcome.pr } : {} };
|
|
926
1040
|
}
|
|
927
|
-
async function processReviseJob(ctx, dir, resumed) {
|
|
1041
|
+
async function processReviseJob(ctx, dir, resumed, abort) {
|
|
928
1042
|
console.log(`
|
|
929
1043
|
\u25B6 Revise ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
930
1044
|
const installResult = await installDependencies(dir);
|
|
@@ -933,11 +1047,12 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
933
1047
|
prompt: buildRevisePrompt(ctx),
|
|
934
1048
|
permissionMode: ctx.permissionMode,
|
|
935
1049
|
model: ORCHESTRATOR_MODEL,
|
|
936
|
-
maxTurns: ORCHESTRATOR_MAX_TURNS
|
|
1050
|
+
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1051
|
+
abortController: abort
|
|
937
1052
|
});
|
|
938
1053
|
const summary = result.text.trim();
|
|
939
1054
|
let reply = summary || "(the agent produced no reply)";
|
|
940
|
-
if (result.
|
|
1055
|
+
if (result.plans?.length) reply = result.plans[0] ?? reply;
|
|
941
1056
|
if (installResult.status === "failed") {
|
|
942
1057
|
reply += `
|
|
943
1058
|
|
|
@@ -955,18 +1070,62 @@ async function processReviseJob(ctx, dir, resumed) {
|
|
|
955
1070
|
cwd: dir,
|
|
956
1071
|
prompt: buildDocumentPrompt(ctx),
|
|
957
1072
|
permissionMode: ctx.permissionMode,
|
|
958
|
-
maxTurns: DOCUMENT_MAX_TURNS
|
|
1073
|
+
maxTurns: DOCUMENT_MAX_TURNS,
|
|
1074
|
+
abortController: abort
|
|
959
1075
|
});
|
|
960
1076
|
documented = true;
|
|
961
1077
|
} catch (err) {
|
|
962
1078
|
console.warn(` wiki update skipped: ${errorMessage2(err)}`);
|
|
963
1079
|
}
|
|
964
1080
|
}
|
|
965
|
-
const outcome = await pushAndOpenPr(ctx, dir, { rebase: !resumed });
|
|
1081
|
+
const outcome = await pushAndOpenPr(ctx, dir, abort, { rebase: !resumed });
|
|
966
1082
|
if (outcome.kind !== "none") {
|
|
967
1083
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented });
|
|
968
1084
|
}
|
|
969
|
-
return {
|
|
1085
|
+
return {
|
|
1086
|
+
text: reply,
|
|
1087
|
+
widgets: [],
|
|
1088
|
+
...outcome.kind === "pr" ? { pr: outcome.pr } : {},
|
|
1089
|
+
...result.plans?.length ? { plans: result.plans } : {}
|
|
1090
|
+
};
|
|
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 } : {} };
|
|
970
1129
|
}
|
|
971
1130
|
async function heartbeat(config) {
|
|
972
1131
|
const health = await checkClaudeCode();
|
|
@@ -986,37 +1145,78 @@ async function pollLoop(config) {
|
|
|
986
1145
|
const swept = await sweepWorkspaces();
|
|
987
1146
|
if (swept > 0) console.log(`Cleared ${swept} stale workspace${swept === 1 ? "" : "s"}.`);
|
|
988
1147
|
await heartbeat(config);
|
|
989
|
-
let
|
|
990
|
-
|
|
991
|
-
|
|
1148
|
+
let heartbeatTimer;
|
|
1149
|
+
const scheduleHeartbeat = () => {
|
|
1150
|
+
heartbeatTimer = setTimeout(async () => {
|
|
992
1151
|
await heartbeat(config);
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1152
|
+
scheduleHeartbeat();
|
|
1153
|
+
}, HEARTBEAT_MS);
|
|
1154
|
+
};
|
|
1155
|
+
scheduleHeartbeat();
|
|
1156
|
+
try {
|
|
1157
|
+
for (; ; ) {
|
|
1158
|
+
let ctx = null;
|
|
1159
|
+
try {
|
|
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();
|
|
1014
1187
|
try {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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;
|
|
1018
1216
|
}
|
|
1019
1217
|
}
|
|
1218
|
+
} finally {
|
|
1219
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
1020
1220
|
}
|
|
1021
1221
|
}
|
|
1022
1222
|
function sleep(ms) {
|
package/package.json
CHANGED
|
@@ -84,13 +84,13 @@ plan without re-deriving it.
|
|
|
84
84
|
|
|
85
85
|
### Multiple plans per request
|
|
86
86
|
|
|
87
|
-
A single request can yield **several** plans — one thread can be accepted into
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
commenting to refine it; treat a later turn as a fresh **Plan** phase and call
|
|
93
|
-
`submit_plan` again with the revised fields.
|
|
87
|
+
A single request can yield **several** plans — one thread can be accepted into many. If the
|
|
88
|
+
work naturally splits into independent pieces, or the user asks for more than one plan, make
|
|
89
|
+
**ONE `submit_plan` call** with all of them in the `plans[]` array (one entry per plan, each
|
|
90
|
+
with a distinct title). Do **not** call `submit_plan` more than once. Each entry becomes its
|
|
91
|
+
own independently-acceptable "Accept as plan" draft. After a plan is accepted the user may
|
|
92
|
+
keep commenting to refine it; treat a later turn as a fresh **Plan** phase and call
|
|
93
|
+
`submit_plan` again with a `plans[]` array containing the revised fields.
|
|
94
94
|
|
|
95
95
|
## Always
|
|
96
96
|
|
|
@@ -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.
|
|
@@ -39,11 +39,11 @@ actual code. Pick exactly one:
|
|
|
39
39
|
Explain why in plain prose, offer an alternative if you have one, and end your
|
|
40
40
|
turn. Make no code changes.
|
|
41
41
|
- **Re-plan** — the request meaningfully changes scope or direction, enough that a
|
|
42
|
-
fresh plan should be agreed before building. Call **`submit_plan`** with
|
|
43
|
-
revised structured fields (same shape as the request-to-plan skill:
|
|
44
|
-
`goal`, `assumptions`, `steps`, `acceptanceCriteria` — at least 2 —, `risks`,
|
|
45
|
-
`outOfScope`). The runner posts it as a revision
|
|
46
|
-
code changes this turn.
|
|
42
|
+
fresh plan should be agreed before building. Call **`submit_plan`** with a `plans[]` array
|
|
43
|
+
containing the revised structured fields (same per-plan shape as the request-to-plan skill:
|
|
44
|
+
`scope`, `goal`, `assumptions`, `steps`, `acceptanceCriteria` — at least 2 —, `risks`,
|
|
45
|
+
`outOfScope`). Include only one entry for a revise turn. The runner posts it as a revision
|
|
46
|
+
the user can accept; make no code changes this turn.
|
|
47
47
|
- **Implement** — the request is clear and reasonable. Make the change (via
|
|
48
48
|
subagents — see Step 2). This is the common case for small fine-tuning.
|
|
49
49
|
|