@neriros/ralphy 3.2.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/mcp/index.js +69 -2
- package/dist/shell/index.js +1065 -300
- package/package.json +1 -1
package/dist/shell/index.js
CHANGED
|
@@ -18928,8 +18928,8 @@ import { readFileSync } from "fs";
|
|
|
18928
18928
|
import { resolve } from "path";
|
|
18929
18929
|
function getVersion() {
|
|
18930
18930
|
try {
|
|
18931
|
-
if ("3.
|
|
18932
|
-
return "3.
|
|
18931
|
+
if ("3.3.1")
|
|
18932
|
+
return "3.3.1";
|
|
18933
18933
|
} catch {}
|
|
18934
18934
|
const dirsToTry = [];
|
|
18935
18935
|
try {
|
|
@@ -59219,11 +59219,10 @@ var init_use_app = __esm(() => {
|
|
|
59219
59219
|
});
|
|
59220
59220
|
|
|
59221
59221
|
// node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/hooks/use-stdout.js
|
|
59222
|
-
var import_react18
|
|
59222
|
+
var import_react18;
|
|
59223
59223
|
var init_use_stdout = __esm(() => {
|
|
59224
59224
|
init_StdoutContext();
|
|
59225
59225
|
import_react18 = __toESM(require_react(), 1);
|
|
59226
|
-
use_stdout_default = useStdout;
|
|
59227
59226
|
});
|
|
59228
59227
|
|
|
59229
59228
|
// node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/hooks/use-stderr.js
|
|
@@ -59448,6 +59447,50 @@ function runOpenspec(args, options = {}) {
|
|
|
59448
59447
|
stderr: proc.stderr ? decoder.decode(proc.stderr) : ""
|
|
59449
59448
|
};
|
|
59450
59449
|
}
|
|
59450
|
+
function appendSteeringTaskToTasksMd(existing, taskLine) {
|
|
59451
|
+
const SECTION = "## Steering";
|
|
59452
|
+
const trimmed = existing.replace(/\s+$/, "");
|
|
59453
|
+
if (trimmed.length === 0) {
|
|
59454
|
+
return `${SECTION}
|
|
59455
|
+
|
|
59456
|
+
${taskLine}
|
|
59457
|
+
`;
|
|
59458
|
+
}
|
|
59459
|
+
const lines = trimmed.split(/\r?\n/);
|
|
59460
|
+
let sectionStart = -1;
|
|
59461
|
+
for (let i = 0;i < lines.length; i += 1) {
|
|
59462
|
+
if (/^##\s+Steering\s*$/i.test(lines[i])) {
|
|
59463
|
+
sectionStart = i;
|
|
59464
|
+
break;
|
|
59465
|
+
}
|
|
59466
|
+
}
|
|
59467
|
+
if (sectionStart === -1) {
|
|
59468
|
+
return `${trimmed}
|
|
59469
|
+
|
|
59470
|
+
${SECTION}
|
|
59471
|
+
|
|
59472
|
+
${taskLine}
|
|
59473
|
+
`;
|
|
59474
|
+
}
|
|
59475
|
+
let sectionEnd = lines.length;
|
|
59476
|
+
for (let i = sectionStart + 1;i < lines.length; i += 1) {
|
|
59477
|
+
if (/^##\s+/.test(lines[i])) {
|
|
59478
|
+
sectionEnd = i;
|
|
59479
|
+
break;
|
|
59480
|
+
}
|
|
59481
|
+
}
|
|
59482
|
+
let insertAt = sectionEnd;
|
|
59483
|
+
while (insertAt - 1 > sectionStart && (lines[insertAt - 1] ?? "").trim() === "") {
|
|
59484
|
+
insertAt -= 1;
|
|
59485
|
+
}
|
|
59486
|
+
const before2 = lines.slice(0, insertAt);
|
|
59487
|
+
const after2 = lines.slice(insertAt);
|
|
59488
|
+
const out = [...before2, taskLine, ...after2.length ? [""] : [], ...after2].join(`
|
|
59489
|
+
`);
|
|
59490
|
+
return out.endsWith(`
|
|
59491
|
+
`) ? out : `${out}
|
|
59492
|
+
`;
|
|
59493
|
+
}
|
|
59451
59494
|
|
|
59452
59495
|
class OpenSpecChangeStore {
|
|
59453
59496
|
async createChange(name, description) {
|
|
@@ -59504,6 +59547,16 @@ ${existing.trimStart()}` : `${message}
|
|
|
59504
59547
|
`;
|
|
59505
59548
|
await mkdir(dirname4(path), { recursive: true });
|
|
59506
59549
|
await Bun.write(path, updated);
|
|
59550
|
+
const firstLine = message.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? message.trim();
|
|
59551
|
+
if (firstLine.length === 0)
|
|
59552
|
+
return;
|
|
59553
|
+
const tasksPath = join6("openspec", "changes", name, "tasks.md");
|
|
59554
|
+
const tasksFile = Bun.file(tasksPath);
|
|
59555
|
+
const existingTasks = await tasksFile.exists() ? await tasksFile.text() : "";
|
|
59556
|
+
const taskLine = `- [ ] Address steering: ${firstLine}`;
|
|
59557
|
+
const next = appendSteeringTaskToTasksMd(existingTasks, taskLine);
|
|
59558
|
+
await mkdir(dirname4(tasksPath), { recursive: true });
|
|
59559
|
+
await Bun.write(tasksPath, next);
|
|
59507
59560
|
}
|
|
59508
59561
|
async readSection(name, artifact, heading) {
|
|
59509
59562
|
const file = Bun.file(join6("openspec", "changes", name, artifact));
|
|
@@ -63850,7 +63903,12 @@ var init_types2 = __esm(() => {
|
|
|
63850
63903
|
createPr: exports_external.boolean().default(false),
|
|
63851
63904
|
usage: UsageSchema.default({}),
|
|
63852
63905
|
history: exports_external.array(HistoryEntrySchema).default([]),
|
|
63853
|
-
metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
|
|
63906
|
+
metadata: exports_external.object({ branch: exports_external.string().optional() }).default({}),
|
|
63907
|
+
linearComments: exports_external.object({
|
|
63908
|
+
planCommentId: exports_external.string().nullable().default(null),
|
|
63909
|
+
tasksCommentId: exports_external.string().nullable().default(null),
|
|
63910
|
+
planPostedAt: exports_external.string().nullable().default(null)
|
|
63911
|
+
}).default({ planCommentId: null, tasksCommentId: null, planPostedAt: null })
|
|
63854
63912
|
});
|
|
63855
63913
|
PhaseFrontmatterSchema = exports_external.object({
|
|
63856
63914
|
name: exports_external.string(),
|
|
@@ -63888,6 +63946,21 @@ function readState(changeDir) {
|
|
|
63888
63946
|
throw new Error(".ralph-state.json not found");
|
|
63889
63947
|
return StateSchema.parse(JSON.parse(raw));
|
|
63890
63948
|
}
|
|
63949
|
+
function tryReadStateRaw(changeDir) {
|
|
63950
|
+
const filePath = join7(changeDir, STATE_FILE2);
|
|
63951
|
+
const text = getStorage().read(filePath);
|
|
63952
|
+
if (text === null)
|
|
63953
|
+
return { state: null, raw: null };
|
|
63954
|
+
let parsed;
|
|
63955
|
+
try {
|
|
63956
|
+
parsed = JSON.parse(text);
|
|
63957
|
+
} catch {
|
|
63958
|
+
return { state: null, raw: null };
|
|
63959
|
+
}
|
|
63960
|
+
const raw = parsed && typeof parsed === "object" ? parsed : {};
|
|
63961
|
+
const result2 = StateSchema.safeParse(parsed);
|
|
63962
|
+
return { state: result2.success ? result2.data : null, raw };
|
|
63963
|
+
}
|
|
63891
63964
|
function writeState(changeDir, state) {
|
|
63892
63965
|
const filePath = join7(changeDir, STATE_FILE2);
|
|
63893
63966
|
getStorage().write(filePath, JSON.stringify(state, null, 2) + `
|
|
@@ -68802,21 +68875,26 @@ function readSize() {
|
|
|
68802
68875
|
rows: process.stdout.rows ?? 24
|
|
68803
68876
|
};
|
|
68804
68877
|
}
|
|
68878
|
+
function clearScreenAndScrollback() {
|
|
68879
|
+
if (process.stdout.isTTY)
|
|
68880
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
68881
|
+
}
|
|
68805
68882
|
function useTerminalSize() {
|
|
68806
|
-
const
|
|
68807
|
-
|
|
68808
|
-
|
|
68809
|
-
}));
|
|
68883
|
+
const initial2 = import_react53.useRef({ ...readSize(), resizeKey: 0 });
|
|
68884
|
+
const [size2, setSize] = import_react53.useState(initial2.current);
|
|
68885
|
+
const sizeRef = import_react53.useRef(initial2.current);
|
|
68810
68886
|
import_react53.useEffect(() => {
|
|
68811
68887
|
if (!process.stdout.isTTY)
|
|
68812
68888
|
return;
|
|
68813
68889
|
const onResize = () => {
|
|
68814
68890
|
const { columns, rows } = readSize();
|
|
68815
|
-
|
|
68816
|
-
|
|
68817
|
-
|
|
68818
|
-
|
|
68819
|
-
}
|
|
68891
|
+
const prev = sizeRef.current;
|
|
68892
|
+
if (prev.columns === columns && prev.rows === rows)
|
|
68893
|
+
return;
|
|
68894
|
+
clearScreenAndScrollback();
|
|
68895
|
+
const next = { columns, rows, resizeKey: prev.resizeKey + 1 };
|
|
68896
|
+
sizeRef.current = next;
|
|
68897
|
+
setSize(next);
|
|
68820
68898
|
};
|
|
68821
68899
|
process.stdout.on("resize", onResize);
|
|
68822
68900
|
return () => {
|
|
@@ -70550,9 +70628,9 @@ function useLoop(opts) {
|
|
|
70550
70628
|
const tasksDir = join11(opts.tasksDir, opts.name);
|
|
70551
70629
|
const storage = getStorage();
|
|
70552
70630
|
let currentState;
|
|
70553
|
-
const
|
|
70554
|
-
if (
|
|
70555
|
-
currentState =
|
|
70631
|
+
const { state: parsedState, raw: rawState } = tryReadStateRaw(stateDir);
|
|
70632
|
+
if (parsedState !== null) {
|
|
70633
|
+
currentState = parsedState;
|
|
70556
70634
|
if (currentState.engine !== opts.engine || currentState.model !== opts.model) {
|
|
70557
70635
|
currentState = {
|
|
70558
70636
|
...currentState,
|
|
@@ -70562,6 +70640,9 @@ function useLoop(opts) {
|
|
|
70562
70640
|
writeState(stateDir, currentState);
|
|
70563
70641
|
}
|
|
70564
70642
|
} else {
|
|
70643
|
+
if (rawState !== null) {
|
|
70644
|
+
addInfo(`.ralph-state.json was malformed \u2014 reinitialising. External fields (linearComments) preserved.`);
|
|
70645
|
+
}
|
|
70565
70646
|
currentState = buildInitialState({
|
|
70566
70647
|
name: opts.name,
|
|
70567
70648
|
prompt: opts.prompt,
|
|
@@ -70570,6 +70651,9 @@ function useLoop(opts) {
|
|
|
70570
70651
|
manualTest: opts.manualTest,
|
|
70571
70652
|
createPr: opts.createPr ?? false
|
|
70572
70653
|
});
|
|
70654
|
+
if (rawState !== null && rawState.linearComments) {
|
|
70655
|
+
currentState.linearComments = rawState.linearComments;
|
|
70656
|
+
}
|
|
70573
70657
|
writeState(stateDir, currentState);
|
|
70574
70658
|
}
|
|
70575
70659
|
const isResume2 = currentState.iteration > 0;
|
|
@@ -70598,6 +70682,27 @@ function useLoop(opts) {
|
|
|
70598
70682
|
}
|
|
70599
70683
|
const tasksContent = storage.read(join11(tasksDir, MISSION_TASKS_FILENAME));
|
|
70600
70684
|
const agentTasksContent = storage.read(join11(tasksDir, AGENT_TASKS_FILENAME));
|
|
70685
|
+
if (tasksContent === null && currentState.iteration > 0 && typeof opts.changeStore.listChanges === "function") {
|
|
70686
|
+
let stillActive = true;
|
|
70687
|
+
try {
|
|
70688
|
+
const active = await opts.changeStore.listChanges();
|
|
70689
|
+
stillActive = active.includes(opts.name);
|
|
70690
|
+
} catch {
|
|
70691
|
+
stillActive = true;
|
|
70692
|
+
}
|
|
70693
|
+
if (!stillActive) {
|
|
70694
|
+
addInfo(`tasks.md not found and change "${opts.name}" is no longer active \u2014 it was archived externally. Exiting.`);
|
|
70695
|
+
currentState = {
|
|
70696
|
+
...currentState,
|
|
70697
|
+
status: "completed",
|
|
70698
|
+
lastModified: new Date().toISOString()
|
|
70699
|
+
};
|
|
70700
|
+
writeState(stateDir, currentState);
|
|
70701
|
+
setState(currentState);
|
|
70702
|
+
finalStopReason = "completed";
|
|
70703
|
+
break;
|
|
70704
|
+
}
|
|
70705
|
+
}
|
|
70601
70706
|
if (tasksContent !== null) {
|
|
70602
70707
|
const remaining = countUnchecked(tasksContent);
|
|
70603
70708
|
const agentRemaining = agentTasksContent !== null ? countUnchecked(agentTasksContent) : 0;
|
|
@@ -70863,14 +70968,8 @@ function TaskLoop({ opts }) {
|
|
|
70863
70968
|
const { exit } = use_app_default();
|
|
70864
70969
|
const loop = useLoop(opts);
|
|
70865
70970
|
const { isRawModeSupported } = use_stdin_default();
|
|
70866
|
-
const { stdout } = use_stdout_default();
|
|
70867
70971
|
const { resizeKey } = useTerminalSize();
|
|
70868
70972
|
const bannerItem = import_react56.useRef({ id: "__banner__", kind: "banner" });
|
|
70869
|
-
import_react56.useEffect(() => {
|
|
70870
|
-
if (resizeKey === 0)
|
|
70871
|
-
return;
|
|
70872
|
-
stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
70873
|
-
}, [resizeKey, stdout]);
|
|
70874
70973
|
const feedItems = import_react56.useMemo(() => [
|
|
70875
70974
|
bannerItem.current,
|
|
70876
70975
|
...loop.logLines.map((e) => ({ id: e.id, kind: "entry", entry: e }))
|
|
@@ -92410,18 +92509,18 @@ var init_zod2 = __esm(() => {
|
|
|
92410
92509
|
});
|
|
92411
92510
|
|
|
92412
92511
|
// packages/workflow/src/schema.ts
|
|
92413
|
-
var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, BoundariesSchema, WorkflowConfigSchema;
|
|
92512
|
+
var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
|
|
92414
92513
|
var init_schema = __esm(() => {
|
|
92415
92514
|
init_zod2();
|
|
92416
92515
|
MarkerSchema = exports_external2.object({
|
|
92417
|
-
type: exports_external2.enum(["label", "status", "attachment"]),
|
|
92516
|
+
type: exports_external2.enum(["label", "status", "attachment", "project"]),
|
|
92418
92517
|
value: exports_external2.string().min(1)
|
|
92419
92518
|
});
|
|
92420
92519
|
GetIndicatorSchema = exports_external2.object({
|
|
92421
92520
|
filter: exports_external2.array(MarkerSchema).default([])
|
|
92422
92521
|
});
|
|
92423
92522
|
SetIndicatorSchema = exports_external2.union([exports_external2.array(MarkerSchema).min(1), MarkerSchema]);
|
|
92424
|
-
IndicatorsSchema = exports_external2.object({
|
|
92523
|
+
IndicatorsSchema = exports_external2.preprocess((v) => v == null ? {} : v, exports_external2.object({
|
|
92425
92524
|
getTodo: GetIndicatorSchema.optional(),
|
|
92426
92525
|
getInProgress: GetIndicatorSchema.optional(),
|
|
92427
92526
|
getConflicted: GetIndicatorSchema.optional(),
|
|
@@ -92450,7 +92549,7 @@ var init_schema = __esm(() => {
|
|
|
92450
92549
|
}
|
|
92451
92550
|
}
|
|
92452
92551
|
}
|
|
92453
|
-
});
|
|
92552
|
+
}));
|
|
92454
92553
|
ProjectSchema = exports_external2.object({
|
|
92455
92554
|
name: exports_external2.string().optional(),
|
|
92456
92555
|
language: exports_external2.string().optional(),
|
|
@@ -92462,9 +92561,17 @@ var init_schema = __esm(() => {
|
|
|
92462
92561
|
build: exports_external2.string().optional(),
|
|
92463
92562
|
typecheck: exports_external2.string().optional()
|
|
92464
92563
|
}).catchall(exports_external2.string()).default({});
|
|
92564
|
+
DEFAULT_META_ONLY_FILES = [
|
|
92565
|
+
"openspec/**",
|
|
92566
|
+
".ralph/**",
|
|
92567
|
+
"**/agent-tasks.md",
|
|
92568
|
+
"**/tasks.md",
|
|
92569
|
+
"**/MANUAL_TESTING*.md"
|
|
92570
|
+
];
|
|
92465
92571
|
BoundariesSchema = exports_external2.object({
|
|
92466
|
-
never_touch: exports_external2.array(exports_external2.string()).default([])
|
|
92467
|
-
|
|
92572
|
+
never_touch: exports_external2.array(exports_external2.string()).default([]),
|
|
92573
|
+
meta_only_files: exports_external2.array(exports_external2.string()).default(DEFAULT_META_ONLY_FILES)
|
|
92574
|
+
}).strict().default({ never_touch: [], meta_only_files: DEFAULT_META_ONLY_FILES });
|
|
92468
92575
|
WorkflowConfigSchema = exports_external2.object({
|
|
92469
92576
|
project: ProjectSchema,
|
|
92470
92577
|
commands: CommandsSchema,
|
|
@@ -92501,20 +92608,20 @@ var init_schema = __esm(() => {
|
|
|
92501
92608
|
assignee: exports_external2.string().optional(),
|
|
92502
92609
|
postComments: exports_external2.boolean().default(true),
|
|
92503
92610
|
updateEveryIterations: exports_external2.number().int().nonnegative().default(10),
|
|
92504
|
-
mentionTrigger: exports_external2.boolean().default(
|
|
92611
|
+
mentionTrigger: exports_external2.boolean().default(true),
|
|
92505
92612
|
mentionHandle: exports_external2.string().default("@ralphy"),
|
|
92506
|
-
codeReviewTrigger: exports_external2.boolean().default(
|
|
92613
|
+
codeReviewTrigger: exports_external2.boolean().default(true),
|
|
92507
92614
|
codeReviewStaleHours: exports_external2.number().nonnegative().default(24),
|
|
92508
|
-
|
|
92615
|
+
syncTasksToComment: exports_external2.boolean().default(true),
|
|
92509
92616
|
indicators: IndicatorsSchema.default({})
|
|
92510
92617
|
}).strict().default({
|
|
92511
92618
|
postComments: true,
|
|
92512
92619
|
updateEveryIterations: 10,
|
|
92513
|
-
mentionTrigger:
|
|
92620
|
+
mentionTrigger: true,
|
|
92514
92621
|
mentionHandle: "@ralphy",
|
|
92515
|
-
codeReviewTrigger:
|
|
92622
|
+
codeReviewTrigger: true,
|
|
92516
92623
|
codeReviewStaleHours: 24,
|
|
92517
|
-
|
|
92624
|
+
syncTasksToComment: true,
|
|
92518
92625
|
indicators: {}
|
|
92519
92626
|
}),
|
|
92520
92627
|
github: exports_external2.object({
|
|
@@ -92573,70 +92680,71 @@ boundaries:
|
|
|
92573
92680
|
never_touch:
|
|
92574
92681
|
- "dist/**"
|
|
92575
92682
|
- ".claude/worktrees/**"
|
|
92576
|
-
|
|
92683
|
+
# Files that count as "meta only" for the pre-PR substantive-diff guard.
|
|
92684
|
+
# If every changed file matches one of these globs, the loop refuses to
|
|
92685
|
+
# open the PR and respawns the worker \u2014 the actual implementation was
|
|
92686
|
+
# lost (either deleted mid-loop or absorbed by a merge from base).
|
|
92687
|
+
meta_only_files:
|
|
92688
|
+
- "openspec/**"
|
|
92689
|
+
- ".ralph/**"
|
|
92690
|
+
- "**/agent-tasks.md"
|
|
92691
|
+
- "**/tasks.md"
|
|
92692
|
+
- "**/MANUAL_TESTING*.md"
|
|
92693
|
+
|
|
92694
|
+
# \u2500\u2500\u2500 Scheduling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92577
92695
|
# How many tasks to run in parallel.
|
|
92578
92696
|
concurrency: 1
|
|
92579
|
-
|
|
92580
92697
|
# Seconds between polls for new Linear issues (agent mode).
|
|
92581
92698
|
pollIntervalSeconds: 60
|
|
92699
|
+
# Seconds to wait between loop iterations (throttle).
|
|
92700
|
+
iterationDelaySeconds: 0
|
|
92582
92701
|
|
|
92583
|
-
#
|
|
92702
|
+
# \u2500\u2500\u2500 Per-task limits (0 = unlimited) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92584
92703
|
maxIterationsPerTask: 0
|
|
92585
|
-
|
|
92586
|
-
# Maximum cost in USD per task. 0 = unlimited.
|
|
92587
92704
|
maxCostUsdPerTask: 0
|
|
92588
|
-
|
|
92589
|
-
# Maximum wall-clock minutes per task. 0 = unlimited.
|
|
92590
92705
|
maxRuntimeMinutesPerTask: 0
|
|
92591
|
-
|
|
92592
92706
|
# Stop a task after this many consecutive identical failures.
|
|
92593
92707
|
maxConsecutiveFailuresPerTask: 5
|
|
92594
92708
|
|
|
92595
|
-
#
|
|
92596
|
-
|
|
92597
|
-
|
|
92709
|
+
# \u2500\u2500\u2500 Engine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92710
|
+
# Underlying engine: "claude" or "codex".
|
|
92711
|
+
engine: claude
|
|
92712
|
+
# Model tier: "haiku", "sonnet", or "opus".
|
|
92713
|
+
model: opus
|
|
92598
92714
|
# Log the raw engine stream to stdout.
|
|
92599
92715
|
logRawStream: false
|
|
92600
|
-
|
|
92601
92716
|
# Pass --verbose to the ralph task sub-process.
|
|
92602
92717
|
taskVerbose: false
|
|
92603
92718
|
|
|
92719
|
+
# \u2500\u2500\u2500 Worktree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92604
92720
|
# Run each task in an isolated git worktree.
|
|
92605
92721
|
useWorktree: false
|
|
92606
|
-
|
|
92607
92722
|
# Delete the worktree after a successful task.
|
|
92608
92723
|
cleanupWorktreeOnSuccess: false
|
|
92609
92724
|
|
|
92725
|
+
# \u2500\u2500\u2500 Pull requests \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92610
92726
|
# Open a pull request after a task succeeds.
|
|
92611
92727
|
createPrOnSuccess: false
|
|
92612
|
-
|
|
92613
92728
|
# Base branch for pull requests.
|
|
92614
92729
|
prBaseBranch: main
|
|
92615
|
-
|
|
92616
92730
|
# When true, stack dependent issues' PRs onto their blocker's open PR.
|
|
92617
92731
|
stackPrsOnDependencies: false
|
|
92618
|
-
|
|
92619
92732
|
# Strategy used when GitHub auto-merge is enabled.
|
|
92620
92733
|
autoMergeStrategy: squash
|
|
92621
92734
|
|
|
92735
|
+
# \u2500\u2500\u2500 CI auto-fix \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92622
92736
|
# Let the agent attempt to fix CI failures after a PR is created.
|
|
92623
92737
|
fixCiOnFailure: false
|
|
92624
|
-
|
|
92625
92738
|
# Maximum number of CI-fix attempts per task.
|
|
92626
92739
|
maxCiFixAttempts: 5
|
|
92627
|
-
|
|
92628
92740
|
# Seconds between CI status polls.
|
|
92629
92741
|
ciPollIntervalSeconds: 30
|
|
92630
92742
|
|
|
92631
|
-
#
|
|
92632
|
-
|
|
92633
|
-
|
|
92634
|
-
#
|
|
92635
|
-
|
|
92636
|
-
|
|
92637
|
-
# Pre-existing error check: gate the agent when the base branch is already broken.
|
|
92638
|
-
# When enabled, the agent runs these commands against the base branch HEAD before
|
|
92639
|
-
# scheduling new work; failures open a Linear ticket and pause new pickups.
|
|
92743
|
+
# \u2500\u2500\u2500 Base-branch health gate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92744
|
+
# Pre-existing error check: gate the agent when the base branch is already
|
|
92745
|
+
# broken. When enabled, the agent runs these commands against the base
|
|
92746
|
+
# branch HEAD before scheduling new work; failures open a Linear ticket
|
|
92747
|
+
# and pause new pickups.
|
|
92640
92748
|
preExistingErrorCheck:
|
|
92641
92749
|
enabled: false
|
|
92642
92750
|
# Commands to run against the base branch. When empty, falls back to commands.lint / commands.test.
|
|
@@ -92645,38 +92753,49 @@ preExistingErrorCheck:
|
|
|
92645
92753
|
label: "ralph:pre-existing-error"
|
|
92646
92754
|
outputCharLimit: 4000
|
|
92647
92755
|
|
|
92756
|
+
# \u2500\u2500\u2500 Linear integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92648
92757
|
linear:
|
|
92649
92758
|
# Linear team key (e.g. "ENG"). Omit to match all teams.
|
|
92650
92759
|
# team: ENG
|
|
92651
92760
|
|
|
92652
92761
|
# Post progress comments on the Linear issue while a task is running.
|
|
92653
92762
|
postComments: true
|
|
92654
|
-
|
|
92655
92763
|
# Post a progress update every N iterations. 0 disables.
|
|
92656
92764
|
updateEveryIterations: 10
|
|
92657
92765
|
|
|
92658
92766
|
# Watch done-issue comments + linked GitHub PR comments for @ralphy mentions.
|
|
92659
|
-
mentionTrigger:
|
|
92767
|
+
mentionTrigger: true
|
|
92660
92768
|
mentionHandle: "@ralphy"
|
|
92661
92769
|
|
|
92662
92770
|
# Watch open tracked PRs for unresolved review-thread comments.
|
|
92663
|
-
codeReviewTrigger:
|
|
92771
|
+
codeReviewTrigger: true
|
|
92664
92772
|
codeReviewStaleHours: 24
|
|
92665
92773
|
|
|
92666
|
-
# Mirror the loop's tasks.md into
|
|
92667
|
-
#
|
|
92668
|
-
#
|
|
92669
|
-
|
|
92774
|
+
# Mirror the loop's tasks.md into a sticky Linear comment (always the
|
|
92775
|
+
# last comment on the issue). Updates on worker launch, on the same
|
|
92776
|
+
# cadence as updateEveryIterations, and on done-transition.
|
|
92777
|
+
syncTasksToComment: true
|
|
92670
92778
|
|
|
92671
92779
|
# Indicators map Ralph lifecycle events to Linear labels/statuses.
|
|
92672
|
-
#
|
|
92673
|
-
#
|
|
92674
|
-
|
|
92675
|
-
|
|
92780
|
+
#
|
|
92781
|
+
# Filter semantics (per indicator's \`filter:\` list):
|
|
92782
|
+
# \u2022 Entries of the SAME type (e.g. two \`status\` entries) are ORed
|
|
92783
|
+
# \u2014 the issue matches if any value matches.
|
|
92784
|
+
# \u2022 Entries of DIFFERENT types (one \`status\` + one \`label\`) are
|
|
92785
|
+
# ANDed \u2014 the issue must satisfy every type.
|
|
92786
|
+
# Example: a filter with two statuses + one label matches issues
|
|
92787
|
+
# where status \u2208 {A, B} AND label = L.
|
|
92788
|
+
#
|
|
92789
|
+
# Sections below group one state at a time; its get/set/clear sit
|
|
92790
|
+
# adjacent so the lifecycle reads top-to-bottom.
|
|
92791
|
+
indicators:
|
|
92792
|
+
# \u2500\u2500 Todo (pickup trigger) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92676
92793
|
# getTodo:
|
|
92677
92794
|
# filter:
|
|
92678
92795
|
# - type: status
|
|
92679
92796
|
# value: Todo
|
|
92797
|
+
#
|
|
92798
|
+
# \u2500\u2500 In Progress \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92680
92799
|
# getInProgress:
|
|
92681
92800
|
# filter:
|
|
92682
92801
|
# - type: status
|
|
@@ -92685,7 +92804,7 @@ linear:
|
|
|
92685
92804
|
# type: status
|
|
92686
92805
|
# value: In Progress
|
|
92687
92806
|
#
|
|
92688
|
-
#
|
|
92807
|
+
# \u2500\u2500 Done \u2192 Review hand-off \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92689
92808
|
# setDone:
|
|
92690
92809
|
# type: status
|
|
92691
92810
|
# value: In Review
|
|
@@ -92697,7 +92816,7 @@ linear:
|
|
|
92697
92816
|
# type: label
|
|
92698
92817
|
# value: "ralph:review"
|
|
92699
92818
|
#
|
|
92700
|
-
#
|
|
92819
|
+
# \u2500\u2500 Conflicted \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92701
92820
|
# getConflicted:
|
|
92702
92821
|
# filter:
|
|
92703
92822
|
# - type: label
|
|
@@ -92709,16 +92828,27 @@ linear:
|
|
|
92709
92828
|
# type: label
|
|
92710
92829
|
# value: "ralph:conflict"
|
|
92711
92830
|
#
|
|
92712
|
-
#
|
|
92831
|
+
# \u2500\u2500 Auto-merge (opt-in) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92713
92832
|
# getAutoMerge:
|
|
92714
92833
|
# filter:
|
|
92715
92834
|
# - type: label
|
|
92716
92835
|
# value: "ralph:auto-merge"
|
|
92717
92836
|
#
|
|
92718
|
-
#
|
|
92837
|
+
# \u2500\u2500 Error quarantine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92719
92838
|
# setError:
|
|
92720
92839
|
# type: label
|
|
92721
92840
|
# value: "ralph:error"
|
|
92841
|
+
#
|
|
92842
|
+
# # Project-based filter / assignment
|
|
92843
|
+
# # getTodo can filter by Linear project name, and setInProgress can
|
|
92844
|
+
# # reassign the issue into a different project.
|
|
92845
|
+
# getTodo:
|
|
92846
|
+
# filter:
|
|
92847
|
+
# - type: project
|
|
92848
|
+
# value: "Ralph Queue"
|
|
92849
|
+
# setInProgress:
|
|
92850
|
+
# type: project
|
|
92851
|
+
# value: "Ralph In Progress"
|
|
92722
92852
|
---
|
|
92723
92853
|
You are working on {{ issue.identifier }}: {{ issue.title }}.
|
|
92724
92854
|
|
|
@@ -93526,15 +93656,18 @@ function partition2(markers) {
|
|
|
93526
93656
|
const statuses = [];
|
|
93527
93657
|
const labels = [];
|
|
93528
93658
|
const attachmentSubtitles = [];
|
|
93659
|
+
const projects = [];
|
|
93529
93660
|
for (const m of markers) {
|
|
93530
93661
|
if (m.type === "status")
|
|
93531
93662
|
statuses.push(m.value);
|
|
93532
93663
|
else if (m.type === "label")
|
|
93533
93664
|
labels.push(m.value);
|
|
93534
|
-
else
|
|
93665
|
+
else if (m.type === "attachment")
|
|
93535
93666
|
attachmentSubtitles.push(m.value);
|
|
93667
|
+
else if (m.type === "project")
|
|
93668
|
+
projects.push(m.value);
|
|
93536
93669
|
}
|
|
93537
|
-
return { statuses, labels, attachmentSubtitles };
|
|
93670
|
+
return { statuses, labels, attachmentSubtitles, projects };
|
|
93538
93671
|
}
|
|
93539
93672
|
function buildIssueFilter(spec) {
|
|
93540
93673
|
const where = {};
|
|
@@ -93550,7 +93683,7 @@ function buildIssueFilter(spec) {
|
|
|
93550
93683
|
}
|
|
93551
93684
|
const inc = spec.include ?? [];
|
|
93552
93685
|
if (inc.length > 0) {
|
|
93553
|
-
const { statuses, labels, attachmentSubtitles } = partition2(inc);
|
|
93686
|
+
const { statuses, labels, attachmentSubtitles, projects } = partition2(inc);
|
|
93554
93687
|
const branches = [];
|
|
93555
93688
|
if (statuses.length > 0)
|
|
93556
93689
|
branches.push({ state: { name: { in: statuses } } });
|
|
@@ -93566,6 +93699,8 @@ function buildIssueFilter(spec) {
|
|
|
93566
93699
|
}
|
|
93567
93700
|
});
|
|
93568
93701
|
}
|
|
93702
|
+
if (projects.length > 0)
|
|
93703
|
+
branches.push({ project: { name: { in: projects } } });
|
|
93569
93704
|
for (const b of branches)
|
|
93570
93705
|
Object.assign(where, b);
|
|
93571
93706
|
} else {
|
|
@@ -93573,7 +93708,23 @@ function buildIssueFilter(spec) {
|
|
|
93573
93708
|
}
|
|
93574
93709
|
const exc = spec.exclude ?? [];
|
|
93575
93710
|
if (exc.length > 0) {
|
|
93576
|
-
const {
|
|
93711
|
+
const {
|
|
93712
|
+
statuses,
|
|
93713
|
+
labels,
|
|
93714
|
+
attachmentSubtitles: excludedSubtitles,
|
|
93715
|
+
projects: excludedProjects
|
|
93716
|
+
} = partition2(exc);
|
|
93717
|
+
if (excludedProjects.length > 0) {
|
|
93718
|
+
const current = where.project;
|
|
93719
|
+
const noProject = { project: { name: { nin: excludedProjects } } };
|
|
93720
|
+
if (current === undefined)
|
|
93721
|
+
Object.assign(where, noProject);
|
|
93722
|
+
else {
|
|
93723
|
+
const existingAnd = where.and ?? [];
|
|
93724
|
+
where.and = [...existingAnd, { project: current }, noProject];
|
|
93725
|
+
delete where.project;
|
|
93726
|
+
}
|
|
93727
|
+
}
|
|
93577
93728
|
if (excludedSubtitles.length > 0) {
|
|
93578
93729
|
const existingAnd = where.and ?? [];
|
|
93579
93730
|
where.and = [
|
|
@@ -93632,10 +93783,14 @@ async function fetchMentionScanIssues(apiKey, spec) {
|
|
|
93632
93783
|
id identifier title description url priority createdAt
|
|
93633
93784
|
state { name type }
|
|
93634
93785
|
assignee { id email name }
|
|
93786
|
+
project { id name }
|
|
93635
93787
|
labels { nodes { name } }
|
|
93636
93788
|
relations(first: 50) {
|
|
93637
93789
|
nodes { type relatedIssue { id state { type } } }
|
|
93638
93790
|
}
|
|
93791
|
+
comments(first: 50) {
|
|
93792
|
+
nodes { id body createdAt user { name email } }
|
|
93793
|
+
}
|
|
93639
93794
|
}
|
|
93640
93795
|
}
|
|
93641
93796
|
}`;
|
|
@@ -93651,10 +93806,12 @@ async function fetchMentionScanIssues(apiKey, spec) {
|
|
|
93651
93806
|
url: n.url,
|
|
93652
93807
|
state: n.state,
|
|
93653
93808
|
assignee: n.assignee,
|
|
93809
|
+
project: n.project ?? null,
|
|
93654
93810
|
labels: n.labels.nodes.map((l) => l.name),
|
|
93655
93811
|
priority: n.priority,
|
|
93656
93812
|
createdAt: n.createdAt ?? "",
|
|
93657
|
-
blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
|
|
93813
|
+
blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id),
|
|
93814
|
+
comments: n.comments?.nodes ?? []
|
|
93658
93815
|
}));
|
|
93659
93816
|
}
|
|
93660
93817
|
async function fetchOpenIssues(apiKey, spec) {
|
|
@@ -93665,6 +93822,7 @@ async function fetchOpenIssues(apiKey, spec) {
|
|
|
93665
93822
|
id identifier title description url priority createdAt
|
|
93666
93823
|
state { name type }
|
|
93667
93824
|
assignee { id email name }
|
|
93825
|
+
project { id name }
|
|
93668
93826
|
labels { nodes { name } }
|
|
93669
93827
|
relations(first: 50) {
|
|
93670
93828
|
nodes {
|
|
@@ -93687,34 +93845,109 @@ async function fetchOpenIssues(apiKey, spec) {
|
|
|
93687
93845
|
url: n.url,
|
|
93688
93846
|
state: n.state,
|
|
93689
93847
|
assignee: n.assignee,
|
|
93848
|
+
project: n.project ?? null,
|
|
93690
93849
|
labels: n.labels.nodes.map((l) => l.name),
|
|
93691
93850
|
priority: n.priority,
|
|
93692
93851
|
createdAt: n.createdAt ?? "",
|
|
93693
93852
|
blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
|
|
93694
93853
|
}));
|
|
93695
93854
|
}
|
|
93855
|
+
function isRetryableStatus(status) {
|
|
93856
|
+
return status >= 500 && status <= 599;
|
|
93857
|
+
}
|
|
93858
|
+
function parseRetryAfter(header) {
|
|
93859
|
+
if (!header)
|
|
93860
|
+
return;
|
|
93861
|
+
const trimmed = header.trim();
|
|
93862
|
+
if (!trimmed)
|
|
93863
|
+
return;
|
|
93864
|
+
const asNum = Number(trimmed);
|
|
93865
|
+
if (Number.isFinite(asNum))
|
|
93866
|
+
return Math.max(0, asNum * 1000);
|
|
93867
|
+
const asDate = Date.parse(trimmed);
|
|
93868
|
+
if (Number.isFinite(asDate))
|
|
93869
|
+
return Math.max(0, asDate - Date.now());
|
|
93870
|
+
return;
|
|
93871
|
+
}
|
|
93872
|
+
function backoffMs(attempt2) {
|
|
93873
|
+
const base2 = 250 * 2 ** (attempt2 - 1);
|
|
93874
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
93875
|
+
return base2 + jitter;
|
|
93876
|
+
}
|
|
93877
|
+
function isRateLimitedBody(body) {
|
|
93878
|
+
if (typeof body !== "string" || body.length === 0)
|
|
93879
|
+
return false;
|
|
93880
|
+
return body.toLowerCase().includes("rate limit exceeded");
|
|
93881
|
+
}
|
|
93882
|
+
function isRateLimitedError(err) {
|
|
93883
|
+
if (err === null || typeof err !== "object")
|
|
93884
|
+
return false;
|
|
93885
|
+
return err.rateLimited === true;
|
|
93886
|
+
}
|
|
93887
|
+
function formatLinearError(err) {
|
|
93888
|
+
if (err === null || err === undefined)
|
|
93889
|
+
return String(err);
|
|
93890
|
+
if (typeof err !== "object")
|
|
93891
|
+
return String(err);
|
|
93892
|
+
const e = err;
|
|
93893
|
+
const parts = [];
|
|
93894
|
+
if (e.rateLimited)
|
|
93895
|
+
parts.push("rate limited");
|
|
93896
|
+
if (typeof e.status === "number")
|
|
93897
|
+
parts.push(`HTTP ${e.status}`);
|
|
93898
|
+
if (Array.isArray(e.messages) && e.messages.length > 0) {
|
|
93899
|
+
parts.push(`graphql: ${e.messages.join("; ")}`);
|
|
93900
|
+
}
|
|
93901
|
+
if (typeof e.body === "string" && e.body.length > 0 && !e.rateLimited) {
|
|
93902
|
+
const truncated = e.body.length > 200 ? `${e.body.slice(0, 200)}\u2026` : e.body;
|
|
93903
|
+
parts.push(`body: ${truncated}`);
|
|
93904
|
+
}
|
|
93905
|
+
if (parts.length === 0) {
|
|
93906
|
+
if (typeof e.message === "string" && e.message)
|
|
93907
|
+
return e.message;
|
|
93908
|
+
return String(err);
|
|
93909
|
+
}
|
|
93910
|
+
if (typeof e.message === "string" && e.message && !e.rateLimited)
|
|
93911
|
+
parts.unshift(e.message);
|
|
93912
|
+
return parts.join(" \u2014 ");
|
|
93913
|
+
}
|
|
93696
93914
|
async function linearRequest(apiKey, query, variables) {
|
|
93697
|
-
|
|
93698
|
-
|
|
93699
|
-
|
|
93700
|
-
|
|
93701
|
-
|
|
93702
|
-
|
|
93703
|
-
|
|
93704
|
-
|
|
93705
|
-
|
|
93706
|
-
|
|
93707
|
-
|
|
93708
|
-
|
|
93709
|
-
|
|
93710
|
-
|
|
93711
|
-
|
|
93712
|
-
|
|
93713
|
-
|
|
93714
|
-
|
|
93715
|
-
|
|
93915
|
+
let lastHttpError;
|
|
93916
|
+
for (let attempt2 = 1;attempt2 <= MAX_LINEAR_ATTEMPTS; attempt2++) {
|
|
93917
|
+
const res = await fetch(LINEAR_API, {
|
|
93918
|
+
method: "POST",
|
|
93919
|
+
headers: { "Content-Type": "application/json", Authorization: apiKey },
|
|
93920
|
+
body: JSON.stringify({ query, variables })
|
|
93921
|
+
});
|
|
93922
|
+
if (!res.ok) {
|
|
93923
|
+
const err = new Error("Linear API request failed");
|
|
93924
|
+
err.status = res.status;
|
|
93925
|
+
err.body = await res.text();
|
|
93926
|
+
if (res.status === 429 || isRateLimitedBody(err.body)) {
|
|
93927
|
+
err.rateLimited = true;
|
|
93928
|
+
throw err;
|
|
93929
|
+
}
|
|
93930
|
+
lastHttpError = err;
|
|
93931
|
+
if (isRetryableStatus(res.status) && attempt2 < MAX_LINEAR_ATTEMPTS) {
|
|
93932
|
+
const ra = parseRetryAfter(res.headers.get("Retry-After"));
|
|
93933
|
+
const waitMs = Math.min(ra ?? backoffMs(attempt2), MAX_RETRY_AFTER_MS);
|
|
93934
|
+
await linearRequestInternals.sleep(waitMs);
|
|
93935
|
+
continue;
|
|
93936
|
+
}
|
|
93937
|
+
throw err;
|
|
93938
|
+
}
|
|
93939
|
+
const json2 = await res.json();
|
|
93940
|
+
if (json2.errors?.length) {
|
|
93941
|
+
const err = new Error("Linear API returned errors");
|
|
93942
|
+
err.messages = json2.errors.map((e) => e.message);
|
|
93943
|
+
throw err;
|
|
93944
|
+
}
|
|
93945
|
+
if (!json2.data) {
|
|
93946
|
+
throw new Error("Linear API returned no data");
|
|
93947
|
+
}
|
|
93948
|
+
return json2.data;
|
|
93716
93949
|
}
|
|
93717
|
-
|
|
93950
|
+
throw lastHttpError ?? new Error("Linear API request failed");
|
|
93718
93951
|
}
|
|
93719
93952
|
async function addReactionToComment(apiKey, commentId, emoji3) {
|
|
93720
93953
|
const mutation = `mutation Reaction($commentId: String!, $emoji: String!) {
|
|
@@ -93734,6 +93967,36 @@ async function addIssueComment(apiKey, issueId, body) {
|
|
|
93734
93967
|
body
|
|
93735
93968
|
});
|
|
93736
93969
|
}
|
|
93970
|
+
async function createIssueComment(apiKey, issueId, body) {
|
|
93971
|
+
const mutation = `mutation Comment($issueId: String!, $body: String!) {
|
|
93972
|
+
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
93973
|
+
success
|
|
93974
|
+
comment { id }
|
|
93975
|
+
}
|
|
93976
|
+
}`;
|
|
93977
|
+
const data = await linearRequest(apiKey, mutation, { issueId, body });
|
|
93978
|
+
const id = data.commentCreate.comment?.id;
|
|
93979
|
+
if (!id)
|
|
93980
|
+
throw new Error("commentCreate returned no comment id");
|
|
93981
|
+
return id;
|
|
93982
|
+
}
|
|
93983
|
+
async function updateIssueComment(apiKey, commentId, body) {
|
|
93984
|
+
const mutation = `mutation UpdateComment($id: String!, $body: String!) {
|
|
93985
|
+
commentUpdate(id: $id, input: { body: $body }) { success }
|
|
93986
|
+
}`;
|
|
93987
|
+
await linearRequest(apiKey, mutation, {
|
|
93988
|
+
id: commentId,
|
|
93989
|
+
body
|
|
93990
|
+
});
|
|
93991
|
+
}
|
|
93992
|
+
async function deleteIssueComment(apiKey, commentId) {
|
|
93993
|
+
const mutation = `mutation DeleteComment($id: String!) {
|
|
93994
|
+
commentDelete(id: $id) { success }
|
|
93995
|
+
}`;
|
|
93996
|
+
await linearRequest(apiKey, mutation, {
|
|
93997
|
+
id: commentId
|
|
93998
|
+
});
|
|
93999
|
+
}
|
|
93737
94000
|
async function fetchIssueComments(apiKey, issueId) {
|
|
93738
94001
|
const query = `query Comments($id: String!) {
|
|
93739
94002
|
issue(id: $id) {
|
|
@@ -93775,25 +94038,55 @@ async function updateAttachmentSubtitle(apiKey, attachmentId, subtitle) {
|
|
|
93775
94038
|
});
|
|
93776
94039
|
}
|
|
93777
94040
|
async function upsertRalphyAttachment(apiKey, issueId, issueUrl, subtitle) {
|
|
93778
|
-
const attachments = await fetchIssueAttachments(apiKey, issueId
|
|
93779
|
-
|
|
94041
|
+
const attachments = await fetchIssueAttachments(apiKey, issueId, {
|
|
94042
|
+
titleFilter: RALPHY_ATTACHMENT_TITLE
|
|
94043
|
+
});
|
|
94044
|
+
const existing = attachments[0];
|
|
93780
94045
|
if (existing) {
|
|
93781
94046
|
await updateAttachmentSubtitle(apiKey, existing.id, subtitle);
|
|
93782
94047
|
} else {
|
|
93783
94048
|
await createRalphyAttachment(apiKey, issueId, issueUrl, subtitle);
|
|
93784
94049
|
}
|
|
93785
94050
|
}
|
|
93786
|
-
async function fetchIssueAttachments(apiKey, issueId) {
|
|
93787
|
-
const
|
|
94051
|
+
async function fetchIssueAttachments(apiKey, issueId, options) {
|
|
94052
|
+
const titleFilter = options?.titleFilter;
|
|
94053
|
+
const query = titleFilter !== undefined ? `query IssueAttachments($id: String!, $titleFilter: String!) {
|
|
94054
|
+
issue(id: $id) {
|
|
94055
|
+
attachments(filter: { title: { eq: $titleFilter } }, first: 25) {
|
|
94056
|
+
nodes { id url sourceType title }
|
|
94057
|
+
}
|
|
94058
|
+
}
|
|
94059
|
+
}` : `query IssueAttachments($id: String!) {
|
|
93788
94060
|
issue(id: $id) {
|
|
93789
94061
|
attachments(first: 25) {
|
|
93790
94062
|
nodes { id url sourceType title }
|
|
93791
94063
|
}
|
|
93792
94064
|
}
|
|
93793
94065
|
}`;
|
|
93794
|
-
const
|
|
94066
|
+
const variables = titleFilter !== undefined ? { id: issueId, titleFilter } : { id: issueId };
|
|
94067
|
+
const data = await linearRequest(apiKey, query, variables);
|
|
93795
94068
|
return data.issue?.attachments?.nodes ?? [];
|
|
93796
94069
|
}
|
|
94070
|
+
async function fetchAttachmentsForIssues(apiKey, issueIds) {
|
|
94071
|
+
const out = new Map;
|
|
94072
|
+
if (issueIds.length === 0)
|
|
94073
|
+
return out;
|
|
94074
|
+
const query = `query IssuesAttachments($ids: [ID!]!) {
|
|
94075
|
+
issues(filter: { id: { in: $ids } }, first: 250) {
|
|
94076
|
+
nodes {
|
|
94077
|
+
id
|
|
94078
|
+
attachments(first: 25) {
|
|
94079
|
+
nodes { id url sourceType title }
|
|
94080
|
+
}
|
|
94081
|
+
}
|
|
94082
|
+
}
|
|
94083
|
+
}`;
|
|
94084
|
+
const data = await linearRequest(apiKey, query, { ids: issueIds });
|
|
94085
|
+
for (const node2 of data.issues.nodes) {
|
|
94086
|
+
out.set(node2.id, node2.attachments?.nodes ?? []);
|
|
94087
|
+
}
|
|
94088
|
+
return out;
|
|
94089
|
+
}
|
|
93797
94090
|
async function fetchWorkflowStates(apiKey, teamKey) {
|
|
93798
94091
|
const query = `query States($team: String!) {
|
|
93799
94092
|
workflowStates(filter: { team: { key: { eq: $team } } }, first: 50) {
|
|
@@ -93892,14 +94185,40 @@ function issueMatchesGetIndicator(issue2, indicator) {
|
|
|
93892
94185
|
return false;
|
|
93893
94186
|
const labels = new Set(issue2.labels.map((l) => l.toLowerCase()));
|
|
93894
94187
|
const stateName = issue2.state.name.toLowerCase();
|
|
94188
|
+
const projectName = issue2.project?.name.toLowerCase() ?? null;
|
|
93895
94189
|
return indicator.filter.some((m) => {
|
|
93896
94190
|
if (m.type === "label")
|
|
93897
94191
|
return labels.has(m.value.toLowerCase());
|
|
93898
94192
|
if (m.type === "status")
|
|
93899
94193
|
return stateName === m.value.toLowerCase();
|
|
94194
|
+
if (m.type === "project") {
|
|
94195
|
+
if (projectName === null)
|
|
94196
|
+
return false;
|
|
94197
|
+
return projectName === m.value.toLowerCase();
|
|
94198
|
+
}
|
|
93900
94199
|
return false;
|
|
93901
94200
|
});
|
|
93902
94201
|
}
|
|
94202
|
+
async function fetchProjectIdByName(apiKey, name) {
|
|
94203
|
+
const query = `query ProjectId($name: String!) {
|
|
94204
|
+
projects(filter: { name: { eq: $name } }, first: 1) {
|
|
94205
|
+
nodes { id }
|
|
94206
|
+
}
|
|
94207
|
+
}`;
|
|
94208
|
+
const data = await linearRequest(apiKey, query, {
|
|
94209
|
+
name
|
|
94210
|
+
});
|
|
94211
|
+
return data.projects.nodes[0]?.id ?? null;
|
|
94212
|
+
}
|
|
94213
|
+
async function setIssueProject(apiKey, issueId, projectId) {
|
|
94214
|
+
const mutation = `mutation SetProject($id: String!, $projectId: String!) {
|
|
94215
|
+
issueUpdate(id: $id, input: { projectId: $projectId }) { success }
|
|
94216
|
+
}`;
|
|
94217
|
+
await linearRequest(apiKey, mutation, {
|
|
94218
|
+
id: issueId,
|
|
94219
|
+
projectId
|
|
94220
|
+
});
|
|
94221
|
+
}
|
|
93903
94222
|
async function createIssue(apiKey, input) {
|
|
93904
94223
|
const mutation = `mutation CreateIssue($input: IssueCreateInput!) {
|
|
93905
94224
|
issueCreate(input: $input) {
|
|
@@ -93956,7 +94275,12 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
|
|
|
93956
94275
|
labelId
|
|
93957
94276
|
});
|
|
93958
94277
|
}
|
|
93959
|
-
var LINEAR_API = "https://api.linear.app/graphql", RALPHY_ATTACHMENT_TITLE_FILTER = "Ralphy", RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
|
|
94278
|
+
var LINEAR_API = "https://api.linear.app/graphql", RALPHY_ATTACHMENT_TITLE_FILTER = "Ralphy", linearRequestInternals, MAX_LINEAR_ATTEMPTS = 3, MAX_RETRY_AFTER_MS = 2000, RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
|
|
94279
|
+
var init_linear = __esm(() => {
|
|
94280
|
+
linearRequestInternals = {
|
|
94281
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
94282
|
+
};
|
|
94283
|
+
});
|
|
93960
94284
|
|
|
93961
94285
|
// apps/agent/src/sort/compare.ts
|
|
93962
94286
|
function chain(...comparators) {
|
|
@@ -93987,6 +94311,7 @@ function compareQueueEntries(getAutoMerge) {
|
|
|
93987
94311
|
}
|
|
93988
94312
|
var MODE_RANK;
|
|
93989
94313
|
var init_queue_order = __esm(() => {
|
|
94314
|
+
init_linear();
|
|
93990
94315
|
MODE_RANK = {
|
|
93991
94316
|
resume: 0,
|
|
93992
94317
|
"conflict-fix": 1,
|
|
@@ -94428,6 +94753,15 @@ class AgentCoordinator {
|
|
|
94428
94753
|
} catch {}
|
|
94429
94754
|
return true;
|
|
94430
94755
|
}
|
|
94756
|
+
async notifySteeringAppended(changeName, message) {
|
|
94757
|
+
if (!this.deps.onSteeringAppended)
|
|
94758
|
+
return;
|
|
94759
|
+
try {
|
|
94760
|
+
await this.deps.onSteeringAppended(changeName, message);
|
|
94761
|
+
} catch (err) {
|
|
94762
|
+
this.deps.onLog(`! onSteeringAppended failed for ${changeName}: ${err.message}`, "yellow");
|
|
94763
|
+
}
|
|
94764
|
+
}
|
|
94431
94765
|
async notifyExited(issue2, changeName, code, mode) {
|
|
94432
94766
|
const ok = code === 0;
|
|
94433
94767
|
if (this.deps.syncTasks && ok) {
|
|
@@ -94537,6 +94871,7 @@ var emptyPrStatus = () => ({ mergeable: 0, conflicted: 0, ciFailed: 0 }), emptyP
|
|
|
94537
94871
|
prStatus: emptyPrStatus()
|
|
94538
94872
|
});
|
|
94539
94873
|
var init_coordinator = __esm(() => {
|
|
94874
|
+
init_linear();
|
|
94540
94875
|
init_queue_order();
|
|
94541
94876
|
init_src();
|
|
94542
94877
|
});
|
|
@@ -94720,6 +95055,64 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
|
94720
95055
|
}
|
|
94721
95056
|
var init_worktree = () => {};
|
|
94722
95057
|
|
|
95058
|
+
// apps/agent/src/agent/pr-url/index.ts
|
|
95059
|
+
function escapeRegex2(s) {
|
|
95060
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
95061
|
+
}
|
|
95062
|
+
async function discoverPrUrlFromGitHub(identifier, runner, cwd2, onLog) {
|
|
95063
|
+
if (!identifier)
|
|
95064
|
+
return null;
|
|
95065
|
+
const slug = identifier.toLowerCase();
|
|
95066
|
+
let rows;
|
|
95067
|
+
try {
|
|
95068
|
+
const res = await runner.run([
|
|
95069
|
+
"gh",
|
|
95070
|
+
"pr",
|
|
95071
|
+
"list",
|
|
95072
|
+
"--search",
|
|
95073
|
+
`${identifier} in:title`,
|
|
95074
|
+
"--state",
|
|
95075
|
+
"all",
|
|
95076
|
+
"--json",
|
|
95077
|
+
"url,state,headRefName,title,updatedAt"
|
|
95078
|
+
], cwd2);
|
|
95079
|
+
const text = res.stdout.trim();
|
|
95080
|
+
rows = text ? JSON.parse(text) : [];
|
|
95081
|
+
} catch (err) {
|
|
95082
|
+
onLog?.(`! gh pr list (${identifier}) failed: ${err.message}`, "yellow");
|
|
95083
|
+
return null;
|
|
95084
|
+
}
|
|
95085
|
+
const idRe = new RegExp(`\\b${escapeRegex2(identifier)}\\b`, "i");
|
|
95086
|
+
const matches2 = rows.filter((r) => Boolean(r.url) && (idRe.test(r.title ?? "") || (r.headRefName ?? "").toLowerCase().includes(slug)));
|
|
95087
|
+
if (matches2.length === 0)
|
|
95088
|
+
return null;
|
|
95089
|
+
const open = matches2.filter((r) => r.state === "OPEN");
|
|
95090
|
+
const pool = open.length > 0 ? open : matches2;
|
|
95091
|
+
pool.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
95092
|
+
return pool[0]?.url ?? null;
|
|
95093
|
+
}
|
|
95094
|
+
function createPrUrlCache(ttlMs = 5 * 60 * 1000, now2 = Date.now) {
|
|
95095
|
+
const map3 = new Map;
|
|
95096
|
+
return {
|
|
95097
|
+
get(issueId) {
|
|
95098
|
+
const e = map3.get(issueId);
|
|
95099
|
+
if (!e)
|
|
95100
|
+
return;
|
|
95101
|
+
if (now2() - e.fetchedAt >= ttlMs) {
|
|
95102
|
+
map3.delete(issueId);
|
|
95103
|
+
return;
|
|
95104
|
+
}
|
|
95105
|
+
return e.url;
|
|
95106
|
+
},
|
|
95107
|
+
set(issueId, url2) {
|
|
95108
|
+
map3.set(issueId, { url: url2, fetchedAt: now2() });
|
|
95109
|
+
},
|
|
95110
|
+
invalidate(issueId) {
|
|
95111
|
+
map3.delete(issueId);
|
|
95112
|
+
}
|
|
95113
|
+
};
|
|
95114
|
+
}
|
|
95115
|
+
|
|
94723
95116
|
// apps/agent/src/agent/ci.ts
|
|
94724
95117
|
async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
94725
95118
|
let lastErr;
|
|
@@ -94905,11 +95298,79 @@ ${issue2.description.trim()}` : ""
|
|
|
94905
95298
|
].filter(Boolean).join(`
|
|
94906
95299
|
`);
|
|
94907
95300
|
}
|
|
95301
|
+
async function diffFilesAgainstBase(runner, cwd2, base2) {
|
|
95302
|
+
let raw = "";
|
|
95303
|
+
try {
|
|
95304
|
+
const r = await runner.run(["git", "diff", "--name-only", `origin/${base2}...HEAD`], cwd2);
|
|
95305
|
+
raw = r.stdout;
|
|
95306
|
+
} catch {
|
|
95307
|
+
try {
|
|
95308
|
+
const r = await runner.run(["git", "diff", "--name-only", `${base2}...HEAD`], cwd2);
|
|
95309
|
+
raw = r.stdout;
|
|
95310
|
+
} catch {
|
|
95311
|
+
return [];
|
|
95312
|
+
}
|
|
95313
|
+
}
|
|
95314
|
+
return raw.split(`
|
|
95315
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
95316
|
+
}
|
|
95317
|
+
async function classifyDiffAgainstMeta(runner, cwd2, base2, metaOnlyFiles) {
|
|
95318
|
+
const files = await diffFilesAgainstBase(runner, cwd2, base2);
|
|
95319
|
+
if (files.length === 0 || metaOnlyFiles.length === 0) {
|
|
95320
|
+
return { files, onlyMeta: false };
|
|
95321
|
+
}
|
|
95322
|
+
const violations = findBoundaryViolations(files, metaOnlyFiles);
|
|
95323
|
+
const metaSet = new Set(violations.map((v) => v.file));
|
|
95324
|
+
const onlyMeta = files.every((f2) => metaSet.has(f2.replace(/\\/g, "/")));
|
|
95325
|
+
return { files, onlyMeta };
|
|
95326
|
+
}
|
|
95327
|
+
async function branchAlreadyMerged(runner, cwd2, branch, base2) {
|
|
95328
|
+
try {
|
|
95329
|
+
const r = await runner.run([
|
|
95330
|
+
"gh",
|
|
95331
|
+
"pr",
|
|
95332
|
+
"list",
|
|
95333
|
+
"--head",
|
|
95334
|
+
branch,
|
|
95335
|
+
"--state",
|
|
95336
|
+
"merged",
|
|
95337
|
+
"--json",
|
|
95338
|
+
"number",
|
|
95339
|
+
"--jq",
|
|
95340
|
+
".[0].number // empty"
|
|
95341
|
+
], cwd2);
|
|
95342
|
+
if (r.stdout.trim() !== "")
|
|
95343
|
+
return true;
|
|
95344
|
+
} catch {}
|
|
95345
|
+
try {
|
|
95346
|
+
const r = await runner.run(["git", "cherry", base2, "HEAD"], cwd2);
|
|
95347
|
+
const lines = r.stdout.split(`
|
|
95348
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
95349
|
+
if (lines.length > 0 && lines.every((l) => l.startsWith("-")))
|
|
95350
|
+
return true;
|
|
95351
|
+
} catch {}
|
|
95352
|
+
return false;
|
|
95353
|
+
}
|
|
94908
95354
|
async function createPullRequest(input, runner) {
|
|
94909
95355
|
const base2 = input.base ?? "main";
|
|
94910
95356
|
const log2 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
|
|
94911
95357
|
if (log2.stdout.trim() === "")
|
|
94912
95358
|
return null;
|
|
95359
|
+
const metaOnlyFiles = input.metaOnlyFiles ?? [];
|
|
95360
|
+
if (metaOnlyFiles.length > 0) {
|
|
95361
|
+
const classification = await classifyDiffAgainstMeta(runner, input.cwd, base2, metaOnlyFiles);
|
|
95362
|
+
if (classification.onlyMeta && classification.files.length > 0) {
|
|
95363
|
+
if (await branchAlreadyMerged(runner, input.cwd, input.branch, base2)) {
|
|
95364
|
+
return null;
|
|
95365
|
+
}
|
|
95366
|
+
return {
|
|
95367
|
+
url: null,
|
|
95368
|
+
created: false,
|
|
95369
|
+
blocked: "only-meta",
|
|
95370
|
+
blockedFiles: classification.files
|
|
95371
|
+
};
|
|
95372
|
+
}
|
|
95373
|
+
}
|
|
94913
95374
|
await runner.run(["git", "push", "-u", "origin", input.branch], input.cwd);
|
|
94914
95375
|
const existing = await runner.run([
|
|
94915
95376
|
"gh",
|
|
@@ -94934,6 +95395,7 @@ async function createPullRequest(input, runner) {
|
|
|
94934
95395
|
`).pop() ?? "";
|
|
94935
95396
|
return { url: url2, created: true };
|
|
94936
95397
|
}
|
|
95398
|
+
var init_pr = () => {};
|
|
94937
95399
|
|
|
94938
95400
|
// apps/agent/src/agent/post-task.ts
|
|
94939
95401
|
import { join as join20 } from "path";
|
|
@@ -95046,7 +95508,13 @@ async function createPrWithRetry(ctx, issue2) {
|
|
|
95046
95508
|
while (true) {
|
|
95047
95509
|
try {
|
|
95048
95510
|
ctx.emit("pr-create", "git push + gh pr create");
|
|
95049
|
-
pr = await createPullRequest({
|
|
95511
|
+
pr = await createPullRequest({
|
|
95512
|
+
cwd: ctx.cwd,
|
|
95513
|
+
branch: ctx.branch,
|
|
95514
|
+
issue: issue2,
|
|
95515
|
+
base: base2,
|
|
95516
|
+
metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? []
|
|
95517
|
+
}, ctx.cmd);
|
|
95050
95518
|
return { pr, gaveUp: false };
|
|
95051
95519
|
} catch (err) {
|
|
95052
95520
|
const e = err;
|
|
@@ -95312,49 +95780,97 @@ ${indented}${suffix}`, "yellow");
|
|
|
95312
95780
|
}
|
|
95313
95781
|
return PR_FAILED_EXIT;
|
|
95314
95782
|
}
|
|
95315
|
-
const
|
|
95316
|
-
|
|
95317
|
-
|
|
95783
|
+
const maxOuterAttempts = cfg.maxCiFixAttempts;
|
|
95784
|
+
let onlyMetaAttempts = 0;
|
|
95785
|
+
let pr = null;
|
|
95786
|
+
while (true) {
|
|
95787
|
+
const attempt2 = await createPrWithRetry(ctx, issue2);
|
|
95788
|
+
if (attempt2.gaveUp)
|
|
95789
|
+
return PR_FAILED_EXIT;
|
|
95790
|
+
if (attempt2.pr?.blocked === "only-meta") {
|
|
95791
|
+
onlyMetaAttempts += 1;
|
|
95792
|
+
const files = attempt2.pr.blockedFiles ?? [];
|
|
95793
|
+
emit("pr-only-meta", `${files.length} meta file(s)`);
|
|
95794
|
+
log2(`! ${changeName}: branch diff against ${base2} contains only meta files \u2014 implementation appears lost. Refusing to open PR.`, "red");
|
|
95795
|
+
for (const f2 of files)
|
|
95796
|
+
log2(` ${f2}`, "red");
|
|
95797
|
+
if (onlyMetaAttempts > maxOuterAttempts) {
|
|
95798
|
+
log2(`! exceeded ${maxOuterAttempts} only-meta recovery attempts for ${changeName} \u2014 giving up`, "red");
|
|
95799
|
+
return PR_FAILED_EXIT;
|
|
95800
|
+
}
|
|
95801
|
+
const fileList = files.length > 0 ? files.map((f2) => `- ${f2}`).join(`
|
|
95802
|
+
`) : "(empty diff)";
|
|
95803
|
+
const retryCode = await runWorkerWithFixTask(ctx, "Reapply lost implementation files", [
|
|
95804
|
+
`The diff against \`${base2}\` contains only meta files`,
|
|
95805
|
+
`(openspec/tasks.md and similar). The substantive implementation`,
|
|
95806
|
+
`is missing from the branch \u2014 likely deleted by an earlier commit`,
|
|
95807
|
+
`or absorbed by a merge from origin/${base2}.`,
|
|
95808
|
+
"",
|
|
95809
|
+
`Files currently in the diff:`,
|
|
95810
|
+
fileList,
|
|
95811
|
+
"",
|
|
95812
|
+
`Re-apply the actual implementation work the change is supposed`,
|
|
95813
|
+
`to ship. Inspect git history (\`git log ${base2}..HEAD\`) to see`,
|
|
95814
|
+
`what was created earlier and lost, then restore those files`,
|
|
95815
|
+
`(or reproduce the work). Commit the restored files so the next`,
|
|
95816
|
+
`iteration's diff against \`${base2}\` contains real code, not`,
|
|
95817
|
+
`just meta files.`
|
|
95818
|
+
].join(`
|
|
95819
|
+
`));
|
|
95820
|
+
if (retryCode !== 0) {
|
|
95821
|
+
log2(`! worker re-run after only-meta block exited code ${retryCode} \u2014 giving up`, "red");
|
|
95822
|
+
return PR_FAILED_EXIT;
|
|
95823
|
+
}
|
|
95824
|
+
continue;
|
|
95825
|
+
}
|
|
95826
|
+
pr = attempt2.pr;
|
|
95827
|
+
break;
|
|
95828
|
+
}
|
|
95318
95829
|
if (!pr) {
|
|
95319
95830
|
log2(` no commits ahead of ${base2} \u2014 skipping PR`, "gray");
|
|
95320
95831
|
return 0;
|
|
95321
95832
|
}
|
|
95322
|
-
|
|
95323
|
-
|
|
95833
|
+
const prUrl = pr.url;
|
|
95834
|
+
if (!prUrl) {
|
|
95835
|
+
log2(`! PR creation returned a null URL for ${changeName} \u2014 giving up`, "red");
|
|
95836
|
+
return PR_FAILED_EXIT;
|
|
95837
|
+
}
|
|
95838
|
+
log2(` ${pr.created ? "opened" : "found existing"} PR: ${prUrl}`, "green");
|
|
95839
|
+
registerPr?.(changeName, prUrl);
|
|
95324
95840
|
let manualMergePending = false;
|
|
95325
95841
|
if (wantAutoMerge) {
|
|
95326
95842
|
const fallbackEnabled = cfg.manualMergeWhenAutoMergeDisabled !== false;
|
|
95327
|
-
const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(
|
|
95843
|
+
const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log2);
|
|
95328
95844
|
if (repoAllowsAutoMerge === false && fallbackEnabled) {
|
|
95329
|
-
log2(` repo has auto-merge disabled \u2014 will poll ${
|
|
95845
|
+
log2(` repo has auto-merge disabled \u2014 will poll ${prUrl} and merge via gh pr merge once checks pass`, "yellow");
|
|
95330
95846
|
manualMergePending = true;
|
|
95331
95847
|
} else {
|
|
95332
95848
|
try {
|
|
95333
|
-
await cmd.run(["gh", "pr", "merge",
|
|
95334
|
-
log2(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${
|
|
95849
|
+
await cmd.run(["gh", "pr", "merge", prUrl, "--auto", `--${cfg.autoMergeStrategy}`], cwd2);
|
|
95850
|
+
log2(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${prUrl}`, "green");
|
|
95335
95851
|
emit("auto-merge-enabled", cfg.autoMergeStrategy);
|
|
95336
95852
|
} catch (err) {
|
|
95337
95853
|
const e = err;
|
|
95338
95854
|
const detail = e.stderr?.trim() || e.message;
|
|
95339
|
-
log2(`! failed to enable auto-merge on ${
|
|
95855
|
+
log2(`! failed to enable auto-merge on ${prUrl}: ${detail}`, "yellow");
|
|
95340
95856
|
if (fallbackEnabled && /auto[- ]merge/i.test(detail)) {
|
|
95341
|
-
log2(` falling back to manual merge after CI passes for ${
|
|
95857
|
+
log2(` falling back to manual merge after CI passes for ${prUrl}`, "yellow");
|
|
95342
95858
|
manualMergePending = true;
|
|
95343
95859
|
}
|
|
95344
95860
|
}
|
|
95345
95861
|
}
|
|
95346
95862
|
}
|
|
95347
|
-
const ciResult = await fixConflictsAndCiLoop(ctx,
|
|
95863
|
+
const ciResult = await fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict);
|
|
95348
95864
|
if (ciResult !== 0)
|
|
95349
95865
|
return ciResult;
|
|
95350
95866
|
if (manualMergePending) {
|
|
95351
95867
|
try {
|
|
95352
|
-
await cmd.run(["gh", "pr", "merge",
|
|
95353
|
-
log2(` manually merged (${cfg.autoMergeStrategy}) ${
|
|
95868
|
+
await cmd.run(["gh", "pr", "merge", prUrl, `--${cfg.autoMergeStrategy}`], cwd2);
|
|
95869
|
+
log2(` manually merged (${cfg.autoMergeStrategy}) ${prUrl}`, "green");
|
|
95354
95870
|
emit("auto-merge-enabled", `manual:${cfg.autoMergeStrategy}`);
|
|
95355
95871
|
} catch (err) {
|
|
95356
95872
|
const e = err;
|
|
95357
|
-
log2(`! manual merge failed for ${
|
|
95873
|
+
log2(`! manual merge failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
|
|
95358
95874
|
}
|
|
95359
95875
|
}
|
|
95360
95876
|
return 0;
|
|
@@ -95455,6 +95971,8 @@ async function runPostTask(input, deps) {
|
|
|
95455
95971
|
var CI_FAILED_EXIT = 70, PR_FAILED_EXIT = 71, repoAutoMergeCache;
|
|
95456
95972
|
var init_post_task = __esm(() => {
|
|
95457
95973
|
init_tasks_md();
|
|
95974
|
+
init_linear();
|
|
95975
|
+
init_pr();
|
|
95458
95976
|
init_ci();
|
|
95459
95977
|
init_worktree();
|
|
95460
95978
|
repoAutoMergeCache = new Map;
|
|
@@ -95721,64 +96239,235 @@ function renderTasksBlock(tasksMd, meta3) {
|
|
|
95721
96239
|
return out.join(`
|
|
95722
96240
|
`);
|
|
95723
96241
|
}
|
|
95724
|
-
|
|
95725
|
-
|
|
95726
|
-
|
|
95727
|
-
|
|
95728
|
-
if (startIdx >= 0 && endIdx >= 0) {
|
|
95729
|
-
const before2 = existing.slice(0, startIdx);
|
|
95730
|
-
const after2 = existing.slice(endIdx + RALPHY_TASKS_END.length);
|
|
95731
|
-
return `${before2}${block}${after2}`;
|
|
95732
|
-
}
|
|
95733
|
-
if (existing.length === 0)
|
|
95734
|
-
return block;
|
|
95735
|
-
const trimmed = existing.replace(/\s+$/, "");
|
|
95736
|
-
return `${trimmed}
|
|
96242
|
+
var RALPHY_TASKS_START = "<!-- ralphy:tasks:start -->", RALPHY_TASKS_END = "<!-- ralphy:tasks:end -->", MAX_CODE_BLOCK_BYTES;
|
|
96243
|
+
var init_linear_sync = __esm(() => {
|
|
96244
|
+
MAX_CODE_BLOCK_BYTES = 2 * 1024;
|
|
96245
|
+
});
|
|
95737
96246
|
|
|
95738
|
-
|
|
96247
|
+
// apps/agent/src/agent/linear-sync/comment-sync.ts
|
|
96248
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
96249
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
96250
|
+
async function readStateJson(statePath) {
|
|
96251
|
+
const file2 = Bun.file(statePath);
|
|
96252
|
+
if (!await file2.exists())
|
|
96253
|
+
return null;
|
|
96254
|
+
try {
|
|
96255
|
+
return await file2.json();
|
|
96256
|
+
} catch {
|
|
96257
|
+
return null;
|
|
96258
|
+
}
|
|
95739
96259
|
}
|
|
95740
|
-
async function
|
|
95741
|
-
|
|
96260
|
+
async function writeStateJson(statePath, state) {
|
|
96261
|
+
await mkdir6(dirname7(statePath), { recursive: true });
|
|
96262
|
+
await Bun.write(statePath, JSON.stringify(state, null, 2) + `
|
|
96263
|
+
`);
|
|
96264
|
+
}
|
|
96265
|
+
function readComments(state) {
|
|
96266
|
+
const raw = state?.linearComments ?? {};
|
|
96267
|
+
return {
|
|
96268
|
+
planCommentId: raw?.planCommentId ?? null,
|
|
96269
|
+
tasksCommentId: raw?.tasksCommentId ?? null,
|
|
96270
|
+
planPostedAt: raw?.planPostedAt ?? null
|
|
96271
|
+
};
|
|
96272
|
+
}
|
|
96273
|
+
async function patchComments(statePath, patch) {
|
|
96274
|
+
const existing = await readStateJson(statePath) ?? {};
|
|
96275
|
+
const current = readComments(existing);
|
|
96276
|
+
const next = { ...current, ...patch };
|
|
96277
|
+
await writeStateJson(statePath, { ...existing, linearComments: next });
|
|
96278
|
+
}
|
|
96279
|
+
function isCommentNotFoundError(err) {
|
|
96280
|
+
if (!err)
|
|
96281
|
+
return false;
|
|
96282
|
+
const candidates = [];
|
|
96283
|
+
const e = err;
|
|
96284
|
+
if (Array.isArray(e.messages))
|
|
96285
|
+
candidates.push(...e.messages);
|
|
96286
|
+
if (typeof e.message === "string")
|
|
96287
|
+
candidates.push(e.message);
|
|
96288
|
+
const text = candidates.join(" ").toLowerCase();
|
|
96289
|
+
return text.includes("not found") || text.includes("could not find") || text.includes("entity not found");
|
|
96290
|
+
}
|
|
96291
|
+
async function readTasksMd(changeDir, log2) {
|
|
96292
|
+
const file2 = Bun.file(join21(changeDir, "tasks.md"));
|
|
95742
96293
|
if (!await file2.exists()) {
|
|
95743
|
-
|
|
96294
|
+
log2(` comment-sync: tasks.md missing in ${changeDir}, skipping`, "gray");
|
|
95744
96295
|
return null;
|
|
95745
96296
|
}
|
|
95746
|
-
let tasksMd;
|
|
95747
96297
|
try {
|
|
95748
|
-
|
|
96298
|
+
return await file2.text();
|
|
95749
96299
|
} catch (err) {
|
|
95750
|
-
|
|
96300
|
+
log2(`! comment-sync: read tasks.md failed: ${err.message}`, "yellow");
|
|
95751
96301
|
return null;
|
|
95752
96302
|
}
|
|
95753
|
-
|
|
95754
|
-
|
|
95755
|
-
|
|
95756
|
-
|
|
95757
|
-
|
|
95758
|
-
|
|
96303
|
+
}
|
|
96304
|
+
function renderTasksCommentBody(tasksMd, changeName, iteration) {
|
|
96305
|
+
return renderTasksBlock(tasksMd, { changeName, iteration });
|
|
96306
|
+
}
|
|
96307
|
+
async function postOrUpdateTasksComment(deps) {
|
|
96308
|
+
const tasksMd = await readTasksMd(deps.changeDir, deps.log);
|
|
96309
|
+
if (!tasksMd)
|
|
95759
96310
|
return null;
|
|
96311
|
+
const body = renderTasksCommentBody(tasksMd, deps.changeName, deps.iteration);
|
|
96312
|
+
const state = await readStateJson(deps.statePath);
|
|
96313
|
+
const comments = readComments(state);
|
|
96314
|
+
if (comments.tasksCommentId) {
|
|
96315
|
+
try {
|
|
96316
|
+
await deps.mutations.updateIssueComment(deps.apiKey, comments.tasksCommentId, body);
|
|
96317
|
+
deps.log(` comment-sync: updated tasks comment for ${deps.changeName}`, "gray");
|
|
96318
|
+
return comments.tasksCommentId;
|
|
96319
|
+
} catch (err) {
|
|
96320
|
+
if (!isCommentNotFoundError(err)) {
|
|
96321
|
+
deps.log(`! comment-sync: updateIssueComment failed: ${err.message}`, "yellow");
|
|
96322
|
+
return null;
|
|
96323
|
+
}
|
|
96324
|
+
deps.log(` comment-sync: tasks comment ${comments.tasksCommentId} not found \u2014 recreating`, "gray");
|
|
96325
|
+
}
|
|
96326
|
+
}
|
|
96327
|
+
let newId;
|
|
96328
|
+
try {
|
|
96329
|
+
newId = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
|
|
96330
|
+
} catch (err) {
|
|
96331
|
+
deps.log(`! comment-sync: createIssueComment failed: ${err.message}`, "yellow");
|
|
96332
|
+
return null;
|
|
96333
|
+
}
|
|
96334
|
+
await patchComments(deps.statePath, { tasksCommentId: newId });
|
|
96335
|
+
deps.log(` comment-sync: created tasks comment for ${deps.changeName}`, "gray");
|
|
96336
|
+
return newId;
|
|
96337
|
+
}
|
|
96338
|
+
function planningComplete(tasksMd) {
|
|
96339
|
+
const lines = tasksMd.split(/\r?\n/);
|
|
96340
|
+
let inPlanning = false;
|
|
96341
|
+
let total = 0;
|
|
96342
|
+
let unchecked = 0;
|
|
96343
|
+
for (const line of lines) {
|
|
96344
|
+
const h = /^##\s+(.+?)\s*$/.exec(line);
|
|
96345
|
+
if (h) {
|
|
96346
|
+
inPlanning = h[1].trim().toLowerCase() === "planning";
|
|
96347
|
+
continue;
|
|
96348
|
+
}
|
|
96349
|
+
if (!inPlanning)
|
|
96350
|
+
continue;
|
|
96351
|
+
const m = /^\s*-\s+\[( |x|X)\]/.exec(line);
|
|
96352
|
+
if (!m)
|
|
96353
|
+
continue;
|
|
96354
|
+
total += 1;
|
|
96355
|
+
if (m[1] === " ")
|
|
96356
|
+
unchecked += 1;
|
|
95760
96357
|
}
|
|
95761
|
-
|
|
95762
|
-
|
|
96358
|
+
return { allChecked: total > 0 && unchecked === 0, total };
|
|
96359
|
+
}
|
|
96360
|
+
async function readFirstParagraph(path) {
|
|
96361
|
+
const file2 = Bun.file(path);
|
|
96362
|
+
if (!await file2.exists())
|
|
96363
|
+
return null;
|
|
96364
|
+
const text = await file2.text();
|
|
96365
|
+
const blocks = text.split(/\r?\n\s*\r?\n/).map((b) => b.trim()).filter((b) => b.length > 0 && !/^#\s/.test(b));
|
|
96366
|
+
return blocks[0] ?? null;
|
|
96367
|
+
}
|
|
96368
|
+
async function readSection(path, heading) {
|
|
96369
|
+
const file2 = Bun.file(path);
|
|
96370
|
+
if (!await file2.exists())
|
|
96371
|
+
return null;
|
|
96372
|
+
const text = await file2.text();
|
|
96373
|
+
const headingRe = new RegExp(`(^|\\n)##\\s+${heading}\\s*\\n`);
|
|
96374
|
+
const m = headingRe.exec(text);
|
|
96375
|
+
if (!m)
|
|
96376
|
+
return null;
|
|
96377
|
+
const start = m.index + m[0].length;
|
|
96378
|
+
const rest2 = text.slice(start);
|
|
96379
|
+
const next = /\n##\s+/.exec(rest2);
|
|
96380
|
+
const body = next ? rest2.slice(0, next.index) : rest2;
|
|
96381
|
+
return body.trim() || null;
|
|
96382
|
+
}
|
|
96383
|
+
async function postPlanCommentOnce(deps) {
|
|
96384
|
+
const state = await readStateJson(deps.statePath);
|
|
96385
|
+
const comments = readComments(state);
|
|
96386
|
+
if (comments.planCommentId)
|
|
96387
|
+
return null;
|
|
96388
|
+
const tasksMd = await readTasksMd(deps.changeDir, deps.log);
|
|
96389
|
+
if (!tasksMd)
|
|
95763
96390
|
return null;
|
|
96391
|
+
const check2 = planningComplete(tasksMd);
|
|
96392
|
+
if (!check2.allChecked)
|
|
96393
|
+
return null;
|
|
96394
|
+
const proposalPath = join21(deps.changeDir, "proposal.md");
|
|
96395
|
+
const why = await readSection(proposalPath, "Why");
|
|
96396
|
+
const whatChanges = await readSection(proposalPath, "What Changes");
|
|
96397
|
+
if (!why && !whatChanges) {
|
|
96398
|
+
deps.log(` comment-sync: proposal.md has no Why/What Changes, skipping plan comment`, "gray");
|
|
96399
|
+
return null;
|
|
96400
|
+
}
|
|
96401
|
+
const designSummary = await readFirstParagraph(join21(deps.changeDir, "design.md"));
|
|
96402
|
+
const parts = [`### ${PLAN_COMMENT_TITLE} \u2014 \`${deps.changeName}\``];
|
|
96403
|
+
if (why) {
|
|
96404
|
+
parts.push("", "**Why**", "", why);
|
|
96405
|
+
}
|
|
96406
|
+
if (whatChanges) {
|
|
96407
|
+
parts.push("", "**What Changes**", "", whatChanges);
|
|
96408
|
+
}
|
|
96409
|
+
if (designSummary) {
|
|
96410
|
+
parts.push("", "**Design**", "", designSummary);
|
|
96411
|
+
}
|
|
96412
|
+
const body = parts.join(`
|
|
96413
|
+
`);
|
|
96414
|
+
let id;
|
|
95764
96415
|
try {
|
|
95765
|
-
await deps.
|
|
95766
|
-
deps.log(` sync-tasks: updated Linear description for ${deps.changeName}`, "gray");
|
|
95767
|
-
return next;
|
|
96416
|
+
id = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
|
|
95768
96417
|
} catch (err) {
|
|
95769
|
-
deps.log(`! sync
|
|
96418
|
+
deps.log(`! comment-sync: plan comment create failed: ${err.message}`, "yellow");
|
|
95770
96419
|
return null;
|
|
95771
96420
|
}
|
|
96421
|
+
await patchComments(deps.statePath, {
|
|
96422
|
+
planCommentId: id,
|
|
96423
|
+
planPostedAt: new Date().toISOString()
|
|
96424
|
+
});
|
|
96425
|
+
deps.log(` comment-sync: posted plan comment for ${deps.changeName}`, "gray");
|
|
96426
|
+
return id;
|
|
95772
96427
|
}
|
|
95773
|
-
|
|
95774
|
-
|
|
95775
|
-
|
|
95776
|
-
|
|
96428
|
+
async function postSteeringAndRefreshTasks(deps) {
|
|
96429
|
+
const firstLine = deps.message.split(/\r?\n/, 1)[0].trim() || deps.message.trim();
|
|
96430
|
+
const steeringBody = `### ${STEERING_COMMENT_TITLE}
|
|
96431
|
+
|
|
96432
|
+
${deps.message.trim()}`;
|
|
96433
|
+
try {
|
|
96434
|
+
await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, steeringBody);
|
|
96435
|
+
deps.log(` comment-sync: posted steering comment (${firstLine})`, "gray");
|
|
96436
|
+
} catch (err) {
|
|
96437
|
+
deps.log(`! comment-sync: steering comment create failed: ${err.message}`, "yellow");
|
|
96438
|
+
}
|
|
96439
|
+
const state = await readStateJson(deps.statePath);
|
|
96440
|
+
const comments = readComments(state);
|
|
96441
|
+
if (comments.tasksCommentId) {
|
|
96442
|
+
try {
|
|
96443
|
+
await deps.mutations.deleteIssueComment(deps.apiKey, comments.tasksCommentId);
|
|
96444
|
+
deps.log(` comment-sync: deleted old tasks comment`, "gray");
|
|
96445
|
+
} catch (err) {
|
|
96446
|
+
if (!isCommentNotFoundError(err)) {
|
|
96447
|
+
deps.log(`! comment-sync: deleteIssueComment failed: ${err.message}`, "yellow");
|
|
96448
|
+
}
|
|
96449
|
+
}
|
|
96450
|
+
await patchComments(deps.statePath, { tasksCommentId: null });
|
|
96451
|
+
}
|
|
96452
|
+
await postOrUpdateTasksComment({
|
|
96453
|
+
apiKey: deps.apiKey,
|
|
96454
|
+
issueId: deps.issueId,
|
|
96455
|
+
statePath: deps.statePath,
|
|
96456
|
+
changeDir: deps.changeDir,
|
|
96457
|
+
changeName: deps.changeName,
|
|
96458
|
+
log: deps.log,
|
|
96459
|
+
mutations: deps.mutations,
|
|
96460
|
+
iteration: deps.iteration
|
|
96461
|
+
});
|
|
96462
|
+
}
|
|
96463
|
+
var PLAN_COMMENT_TITLE = "\uD83D\uDCCB Ralph plan", STEERING_COMMENT_TITLE = "\uD83E\uDDED Ralph steering";
|
|
96464
|
+
var init_comment_sync = __esm(() => {
|
|
96465
|
+
init_linear_sync();
|
|
95777
96466
|
});
|
|
95778
96467
|
|
|
95779
96468
|
// apps/agent/src/agent/wire.ts
|
|
95780
|
-
import { join as
|
|
95781
|
-
import { mkdir as
|
|
96469
|
+
import { join as join22 } from "path";
|
|
96470
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
95782
96471
|
async function pickOpenPrUrlFromAttachments(urls, issueIdent, cmd, cwd2, onLog) {
|
|
95783
96472
|
const candidates = urls.filter((url2) => GITHUB_PR_URL_RE.test(url2));
|
|
95784
96473
|
let sawNonOpenPr = false;
|
|
@@ -95796,6 +96485,46 @@ async function pickOpenPrUrlFromAttachments(urls, issueIdent, cmd, cwd2, onLog)
|
|
|
95796
96485
|
}
|
|
95797
96486
|
return { url: null, sawNonOpenPr };
|
|
95798
96487
|
}
|
|
96488
|
+
async function resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, deps) {
|
|
96489
|
+
const blockerIds = issue2.blockedByIds;
|
|
96490
|
+
if (blockerIds.length === 0)
|
|
96491
|
+
return null;
|
|
96492
|
+
let attachmentsByBlocker;
|
|
96493
|
+
try {
|
|
96494
|
+
attachmentsByBlocker = await fetchAttachmentsForIssues(deps.apiKey, blockerIds);
|
|
96495
|
+
} catch (err) {
|
|
96496
|
+
deps.onLog(`! could not fetch attachments for blockers of ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96497
|
+
return null;
|
|
96498
|
+
}
|
|
96499
|
+
const candidates = [];
|
|
96500
|
+
for (const blockerId of blockerIds) {
|
|
96501
|
+
const attachments = attachmentsByBlocker.get(blockerId) ?? [];
|
|
96502
|
+
const prUrls = attachments.map((a) => a.url).filter((url2) => /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(url2));
|
|
96503
|
+
const openHeads = [];
|
|
96504
|
+
for (const url2 of prUrls) {
|
|
96505
|
+
try {
|
|
96506
|
+
const res = await runner.run(["gh", "pr", "view", url2, "--json", "state,headRefName", "--jq", "."], runnerCwd);
|
|
96507
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
96508
|
+
if (parsed.state === "OPEN" && parsed.headRefName) {
|
|
96509
|
+
openHeads.push(parsed.headRefName);
|
|
96510
|
+
}
|
|
96511
|
+
} catch (err) {
|
|
96512
|
+
deps.onLog(`! gh pr view failed for ${url2} (blocker of ${issue2.identifier}): ${err.message}`, "yellow");
|
|
96513
|
+
}
|
|
96514
|
+
}
|
|
96515
|
+
if (openHeads.length === 1) {
|
|
96516
|
+
candidates.push(openHeads[0]);
|
|
96517
|
+
} else if (openHeads.length > 1) {
|
|
96518
|
+
deps.onLog(` ${issue2.identifier}: blocker ${blockerId} has ${openHeads.length} open PRs \u2014 skipping dependency base resolution`, "gray");
|
|
96519
|
+
}
|
|
96520
|
+
}
|
|
96521
|
+
if (candidates.length === 1)
|
|
96522
|
+
return candidates[0];
|
|
96523
|
+
if (candidates.length > 1) {
|
|
96524
|
+
deps.onLog(` ${issue2.identifier}: ${candidates.length} blockers have open PRs \u2014 falling back to default base`, "gray");
|
|
96525
|
+
}
|
|
96526
|
+
return null;
|
|
96527
|
+
}
|
|
95799
96528
|
function githubReactionSlug(emoji3) {
|
|
95800
96529
|
switch (emoji3) {
|
|
95801
96530
|
case "\uD83D\uDC40":
|
|
@@ -95870,7 +96599,7 @@ ${c.body.trim()}`;
|
|
|
95870
96599
|
].join(`
|
|
95871
96600
|
`);
|
|
95872
96601
|
}
|
|
95873
|
-
function
|
|
96602
|
+
function escapeRegex3(s) {
|
|
95874
96603
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
95875
96604
|
}
|
|
95876
96605
|
function buildMentionTaskBody(trigger, issueUrl) {
|
|
@@ -95942,7 +96671,7 @@ function buildAgentCoordinator(input) {
|
|
|
95942
96671
|
onWorkerOutput,
|
|
95943
96672
|
onWorkerCmd
|
|
95944
96673
|
} = input;
|
|
95945
|
-
const logsDir =
|
|
96674
|
+
const logsDir = join22(projectRoot, ".ralph", "logs");
|
|
95946
96675
|
const concurrency = args.concurrency || cfg.concurrency;
|
|
95947
96676
|
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
95948
96677
|
const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
|
|
@@ -96014,6 +96743,16 @@ function buildAgentCoordinator(input) {
|
|
|
96014
96743
|
} else if (m.type === "attachment") {
|
|
96015
96744
|
await upsertRalphyAttachment(apiKey, issue2.id, issue2.url, m.value);
|
|
96016
96745
|
onLog(` \u2192 ${issue2.identifier} attachment='${m.value}'`, "gray");
|
|
96746
|
+
} else if (m.type === "project") {
|
|
96747
|
+
const projectId = await fetchProjectIdByName(apiKey, m.value);
|
|
96748
|
+
if (!projectId) {
|
|
96749
|
+
const err = new Error("Linear project not found");
|
|
96750
|
+
err.project = m.value;
|
|
96751
|
+
err.issue = issue2.identifier;
|
|
96752
|
+
throw err;
|
|
96753
|
+
}
|
|
96754
|
+
await setIssueProject(apiKey, issue2.id, projectId);
|
|
96755
|
+
onLog(` \u2192 ${issue2.identifier} project='${m.value}'`, "gray");
|
|
96017
96756
|
} else {
|
|
96018
96757
|
const id = await resolveLabelId(issue2, m.value);
|
|
96019
96758
|
if (!id) {
|
|
@@ -96064,7 +96803,9 @@ function buildAgentCoordinator(input) {
|
|
|
96064
96803
|
const prByChange = new Map;
|
|
96065
96804
|
const prUnavailable = new Map;
|
|
96066
96805
|
const PR_UNAVAILABLE_TTL_MS = 10 * 60 * 1000;
|
|
96806
|
+
const prUrlByIssue = createPrUrlCache(5 * 60 * 1000);
|
|
96067
96807
|
const stalePingedAt = new Map;
|
|
96808
|
+
const lastHandledReviewActivity = new Map;
|
|
96068
96809
|
const useWorktree = args.worktree || cfg.useWorktree;
|
|
96069
96810
|
const scriptRunner = input.runners?.runScript ?? (async (cmd, cwd2) => {
|
|
96070
96811
|
const proc = Bun.spawn({
|
|
@@ -96154,8 +96895,8 @@ function buildAgentCoordinator(input) {
|
|
|
96154
96895
|
} else {
|
|
96155
96896
|
changeName = changeNameForIssue(issue2);
|
|
96156
96897
|
const wtLayout = projectLayout(workerCwd);
|
|
96157
|
-
await
|
|
96158
|
-
await
|
|
96898
|
+
await mkdir7(wtLayout.changeDir(changeName), { recursive: true });
|
|
96899
|
+
await mkdir7(wtLayout.taskStateDir(changeName), { recursive: true });
|
|
96159
96900
|
}
|
|
96160
96901
|
cwdByChange.set(changeName, workerCwd);
|
|
96161
96902
|
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
@@ -96164,7 +96905,7 @@ function buildAgentCoordinator(input) {
|
|
|
96164
96905
|
branchByChange.set(changeName, branch);
|
|
96165
96906
|
if (mode === "review") {
|
|
96166
96907
|
const wtLayout = projectLayout(workerCwd);
|
|
96167
|
-
const tasksFile =
|
|
96908
|
+
const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
|
|
96168
96909
|
let body;
|
|
96169
96910
|
let heading;
|
|
96170
96911
|
if (trigger) {
|
|
@@ -96189,7 +96930,7 @@ function buildAgentCoordinator(input) {
|
|
|
96189
96930
|
await reactivateState2(wtLayout.stateFile(changeName), changeName);
|
|
96190
96931
|
} else if (mode === "conflict-fix") {
|
|
96191
96932
|
const wtLayout = projectLayout(workerCwd);
|
|
96192
|
-
const tasksFile =
|
|
96933
|
+
const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
|
|
96193
96934
|
const prUrl = prByChange.get(changeName);
|
|
96194
96935
|
const body = [
|
|
96195
96936
|
`The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
|
|
@@ -96269,7 +97010,7 @@ PR: ${prUrl}` : ""
|
|
|
96269
97010
|
return c;
|
|
96270
97011
|
}
|
|
96271
97012
|
function defaultSpawn(changeName, cmd, cwd2, note) {
|
|
96272
|
-
const logFilePath =
|
|
97013
|
+
const logFilePath = join22(logsDir, `${changeName}.log`);
|
|
96273
97014
|
const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
|
|
96274
97015
|
const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
|
|
96275
97016
|
const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
|
|
@@ -96328,7 +97069,7 @@ PR: ${prUrl}` : ""
|
|
|
96328
97069
|
function spawnWorker(changeName) {
|
|
96329
97070
|
const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
|
|
96330
97071
|
const injected = input.runners?.spawnWorker;
|
|
96331
|
-
const missionTasksPath =
|
|
97072
|
+
const missionTasksPath = join22(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
|
|
96332
97073
|
const prevTasksPromise = (async () => {
|
|
96333
97074
|
const f2 = Bun.file(missionTasksPath);
|
|
96334
97075
|
return await f2.exists() ? await f2.text() : "";
|
|
@@ -96336,7 +97077,7 @@ PR: ${prUrl}` : ""
|
|
|
96336
97077
|
let logFilePath;
|
|
96337
97078
|
let handle;
|
|
96338
97079
|
if (injected) {
|
|
96339
|
-
logFilePath =
|
|
97080
|
+
logFilePath = join22(logsDir, `${changeName}.log`);
|
|
96340
97081
|
handle = injected(buildTaskCmdFor(changeName), cwd2);
|
|
96341
97082
|
} else {
|
|
96342
97083
|
const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `spawn at ${new Date().toISOString()}`);
|
|
@@ -96396,6 +97137,7 @@ PR: ${prUrl}` : ""
|
|
|
96396
97137
|
ignoreCiChecks: cfg.ignoreCiChecks,
|
|
96397
97138
|
stackPrsOnDependencies: args.stackPrs || cfg.stackPrsOnDependencies,
|
|
96398
97139
|
neverTouch: cfg.boundaries.never_touch,
|
|
97140
|
+
metaOnlyFiles: cfg.boundaries.meta_only_files,
|
|
96399
97141
|
manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled
|
|
96400
97142
|
},
|
|
96401
97143
|
respawnWorker: respawn
|
|
@@ -96407,6 +97149,9 @@ PR: ${prUrl}` : ""
|
|
|
96407
97149
|
registerPr: (cn, url2) => {
|
|
96408
97150
|
prByChange.set(cn, url2);
|
|
96409
97151
|
prUnavailable.delete(cn);
|
|
97152
|
+
const issue2 = issueByChange.get(cn);
|
|
97153
|
+
if (issue2)
|
|
97154
|
+
prUrlByIssue.invalidate(issue2.id);
|
|
96410
97155
|
input.onWorkerPr?.(cn, url2);
|
|
96411
97156
|
},
|
|
96412
97157
|
...onWorkerPhase && {
|
|
@@ -96464,6 +97209,7 @@ PR: ${prUrl}` : ""
|
|
|
96464
97209
|
}
|
|
96465
97210
|
if (state && state !== "OPEN") {
|
|
96466
97211
|
markPrUnavailable(changeName);
|
|
97212
|
+
prUrlByIssue.invalidate(issue2.id);
|
|
96467
97213
|
return null;
|
|
96468
97214
|
}
|
|
96469
97215
|
if (m && m !== "UNKNOWN") {
|
|
@@ -96501,32 +97247,9 @@ PR: ${prUrl}` : ""
|
|
|
96501
97247
|
prUnavailable.set(changeName, Date.now() + PR_UNAVAILABLE_TTL_MS);
|
|
96502
97248
|
}
|
|
96503
97249
|
async function discoverPrUrl(issue2, changeName) {
|
|
96504
|
-
const
|
|
96505
|
-
|
|
96506
|
-
|
|
96507
|
-
const res = await cmdRunner.run(args2, projectRoot);
|
|
96508
|
-
const found = res.stdout.trim();
|
|
96509
|
-
return found || null;
|
|
96510
|
-
} catch (err) {
|
|
96511
|
-
onLog(`! gh ${args2[1] ?? ""} failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96512
|
-
return null;
|
|
96513
|
-
}
|
|
96514
|
-
};
|
|
96515
|
-
const byBranch = await tryGh([
|
|
96516
|
-
"gh",
|
|
96517
|
-
"pr",
|
|
96518
|
-
"list",
|
|
96519
|
-
"--head",
|
|
96520
|
-
branch,
|
|
96521
|
-
"--state",
|
|
96522
|
-
"open",
|
|
96523
|
-
"--json",
|
|
96524
|
-
"url",
|
|
96525
|
-
"--jq",
|
|
96526
|
-
".[0].url // empty"
|
|
96527
|
-
]);
|
|
96528
|
-
if (byBranch)
|
|
96529
|
-
return byBranch;
|
|
97250
|
+
const fromGitHub = await discoverPrUrlFromGitHub(issue2.identifier, cmdRunner, projectRoot, onLog);
|
|
97251
|
+
if (fromGitHub)
|
|
97252
|
+
return fromGitHub;
|
|
96530
97253
|
const fromLinear = await discoverPrUrlFromLinear(issue2);
|
|
96531
97254
|
if (fromLinear.url) {
|
|
96532
97255
|
onLog(` ${issue2.identifier}: PR discovered via Linear attachment (${fromLinear.url})`, "gray");
|
|
@@ -96536,48 +97259,12 @@ PR: ${prUrl}` : ""
|
|
|
96536
97259
|
markPrUnavailable(changeName);
|
|
96537
97260
|
return null;
|
|
96538
97261
|
}
|
|
96539
|
-
onLog(` ${issue2.identifier}: no
|
|
97262
|
+
onLog(` ${issue2.identifier}: no PR found via GitHub search or Linear attachments; conflict scan skipped for ${PR_UNAVAILABLE_TTL_MS / 60000}m`, "gray");
|
|
96540
97263
|
markPrUnavailable(changeName);
|
|
96541
97264
|
return null;
|
|
96542
97265
|
}
|
|
96543
97266
|
async function resolveDependencyBaseBranch(issue2, runner, runnerCwd) {
|
|
96544
|
-
|
|
96545
|
-
if (blockerIds.length === 0)
|
|
96546
|
-
return null;
|
|
96547
|
-
const candidates = [];
|
|
96548
|
-
for (const blockerId of blockerIds) {
|
|
96549
|
-
let attachments;
|
|
96550
|
-
try {
|
|
96551
|
-
attachments = await fetchIssueAttachments(apiKey, blockerId);
|
|
96552
|
-
} catch (err) {
|
|
96553
|
-
onLog(`! could not fetch attachments for blocker ${blockerId} of ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96554
|
-
continue;
|
|
96555
|
-
}
|
|
96556
|
-
const prUrls = attachments.map((a) => a.url).filter((url2) => /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(url2));
|
|
96557
|
-
const openHeads = [];
|
|
96558
|
-
for (const url2 of prUrls) {
|
|
96559
|
-
try {
|
|
96560
|
-
const res = await runner.run(["gh", "pr", "view", url2, "--json", "state,headRefName", "--jq", "."], runnerCwd);
|
|
96561
|
-
const parsed = JSON.parse(res.stdout.trim());
|
|
96562
|
-
if (parsed.state === "OPEN" && parsed.headRefName) {
|
|
96563
|
-
openHeads.push(parsed.headRefName);
|
|
96564
|
-
}
|
|
96565
|
-
} catch (err) {
|
|
96566
|
-
onLog(`! gh pr view failed for ${url2} (blocker of ${issue2.identifier}): ${err.message}`, "yellow");
|
|
96567
|
-
}
|
|
96568
|
-
}
|
|
96569
|
-
if (openHeads.length === 1) {
|
|
96570
|
-
candidates.push(openHeads[0]);
|
|
96571
|
-
} else if (openHeads.length > 1) {
|
|
96572
|
-
onLog(` ${issue2.identifier}: blocker ${blockerId} has ${openHeads.length} open PRs \u2014 skipping dependency base resolution`, "gray");
|
|
96573
|
-
}
|
|
96574
|
-
}
|
|
96575
|
-
if (candidates.length === 1)
|
|
96576
|
-
return candidates[0];
|
|
96577
|
-
if (candidates.length > 1) {
|
|
96578
|
-
onLog(` ${issue2.identifier}: ${candidates.length} blockers have open PRs \u2014 falling back to default base`, "gray");
|
|
96579
|
-
}
|
|
96580
|
-
return null;
|
|
97267
|
+
return resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, { apiKey, onLog });
|
|
96581
97268
|
}
|
|
96582
97269
|
async function discoverPrUrlFromLinear(issue2) {
|
|
96583
97270
|
let attachments;
|
|
@@ -96608,19 +97295,24 @@ PR: ${prUrl}` : ""
|
|
|
96608
97295
|
try {
|
|
96609
97296
|
candidates = await fetchMentionScanIssues(apiKey, { team, assignee });
|
|
96610
97297
|
} catch (err) {
|
|
96611
|
-
|
|
97298
|
+
if (isRateLimitedError(err)) {
|
|
97299
|
+
onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
|
|
97300
|
+
return [];
|
|
97301
|
+
}
|
|
97302
|
+
onLog(`! mention scan: fetchMentionScanIssues failed: ${formatLinearError(err)}`, "yellow");
|
|
96612
97303
|
return [];
|
|
96613
97304
|
}
|
|
96614
97305
|
const out = [];
|
|
96615
97306
|
const queued = new Set;
|
|
97307
|
+
let rateLimitedLogged = false;
|
|
97308
|
+
const logRateLimited = () => {
|
|
97309
|
+
if (rateLimitedLogged)
|
|
97310
|
+
return;
|
|
97311
|
+
rateLimitedLogged = true;
|
|
97312
|
+
onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
|
|
97313
|
+
};
|
|
96616
97314
|
for (const issue2 of candidates) {
|
|
96617
|
-
|
|
96618
|
-
try {
|
|
96619
|
-
comments = await fetchIssueComments(apiKey, issue2.id);
|
|
96620
|
-
} catch (err) {
|
|
96621
|
-
onLog(`! mention scan: Linear comments failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96622
|
-
continue;
|
|
96623
|
-
}
|
|
97315
|
+
const comments = issue2.comments ?? [];
|
|
96624
97316
|
const lastRalphPickup = findLastRalphPickupISO(comments);
|
|
96625
97317
|
if (wantMention) {
|
|
96626
97318
|
for (const c of comments) {
|
|
@@ -96643,11 +97335,18 @@ PR: ${prUrl}` : ""
|
|
|
96643
97335
|
try {
|
|
96644
97336
|
await addReactionToComment(apiKey, c.id, "\uD83D\uDC40");
|
|
96645
97337
|
} catch (err) {
|
|
96646
|
-
|
|
97338
|
+
if (isRateLimitedError(err)) {
|
|
97339
|
+
logRateLimited();
|
|
97340
|
+
queued.add(issue2.id);
|
|
97341
|
+
break;
|
|
97342
|
+
}
|
|
97343
|
+
onLog(`! mention scan: Linear reaction failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
|
|
96647
97344
|
}
|
|
96648
97345
|
queued.add(issue2.id);
|
|
96649
97346
|
break;
|
|
96650
97347
|
}
|
|
97348
|
+
if (rateLimitedLogged)
|
|
97349
|
+
break;
|
|
96651
97350
|
if (queued.has(issue2.id))
|
|
96652
97351
|
continue;
|
|
96653
97352
|
}
|
|
@@ -96677,7 +97376,7 @@ PR: ${prUrl}` : ""
|
|
|
96677
97376
|
try {
|
|
96678
97377
|
await addGithubReactionToComment({ owner, repo, kind: "issue" }, c.id, "\uD83D\uDC40");
|
|
96679
97378
|
} catch (err) {
|
|
96680
|
-
onLog(`! mention scan: GitHub reaction failed for ${prUrl}: ${err
|
|
97379
|
+
onLog(`! mention scan: GitHub reaction failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
|
|
96681
97380
|
}
|
|
96682
97381
|
}
|
|
96683
97382
|
queued.add(issue2.id);
|
|
@@ -96707,7 +97406,9 @@ PR: ${prUrl}` : ""
|
|
|
96707
97406
|
const last2 = t.comments[t.comments.length - 1].createdAt;
|
|
96708
97407
|
return last2 > acc ? last2 : acc;
|
|
96709
97408
|
}, "");
|
|
96710
|
-
|
|
97409
|
+
const lastHandled = lastHandledReviewActivity.get(prUrl) ?? null;
|
|
97410
|
+
const effectiveLastHandled = lastRalphPickup && lastHandled ? lastRalphPickup > lastHandled ? lastRalphPickup : lastHandled : lastRalphPickup ?? lastHandled;
|
|
97411
|
+
if (!effectiveLastHandled || newestReviewerActivity > effectiveLastHandled) {
|
|
96711
97412
|
const body = unresolved.map((t) => {
|
|
96712
97413
|
const head3 = t.path ? `_${t.path}${t.line ? `:${t.line}` : ""}_` : "_(general)_";
|
|
96713
97414
|
const lines = t.comments.map((c) => `> **${c.author ?? "reviewer"}** (${c.createdAt})
|
|
@@ -96721,6 +97422,7 @@ PR: ${prUrl}` : ""
|
|
|
96721
97422
|
---
|
|
96722
97423
|
|
|
96723
97424
|
`);
|
|
97425
|
+
lastHandledReviewActivity.set(prUrl, newestReviewerActivity);
|
|
96724
97426
|
return {
|
|
96725
97427
|
source: "github-review",
|
|
96726
97428
|
body,
|
|
@@ -96832,17 +97534,21 @@ PR: ${prUrl}` : ""
|
|
|
96832
97534
|
return latest;
|
|
96833
97535
|
}
|
|
96834
97536
|
function containsHandle(body, handle) {
|
|
96835
|
-
const re = new RegExp(`(^|\\s|[^A-Za-z0-9_])${
|
|
97537
|
+
const re = new RegExp(`(^|\\s|[^A-Za-z0-9_])${escapeRegex3(handle)}\\b`, "i");
|
|
96836
97538
|
return re.test(body);
|
|
96837
97539
|
}
|
|
96838
97540
|
async function resolvePrUrlForIssue(issue2) {
|
|
96839
97541
|
const changeName = changeNameForIssue(issue2);
|
|
96840
97542
|
if (isPrUnavailable(changeName))
|
|
96841
97543
|
return null;
|
|
96842
|
-
const
|
|
96843
|
-
if (
|
|
97544
|
+
const inflight = prByChange.get(changeName);
|
|
97545
|
+
if (inflight)
|
|
97546
|
+
return inflight;
|
|
97547
|
+
const cached2 = prUrlByIssue.get(issue2.id);
|
|
97548
|
+
if (cached2 !== undefined)
|
|
96844
97549
|
return cached2;
|
|
96845
97550
|
const found = await discoverPrUrl(issue2, changeName);
|
|
97551
|
+
prUrlByIssue.set(issue2.id, found);
|
|
96846
97552
|
if (found)
|
|
96847
97553
|
prByChange.set(changeName, found);
|
|
96848
97554
|
return found;
|
|
@@ -96868,10 +97574,16 @@ PR: ${prUrl}` : ""
|
|
|
96868
97574
|
const parsed = JSON.parse(res.stdout || "[]");
|
|
96869
97575
|
return parsed;
|
|
96870
97576
|
} catch (err) {
|
|
96871
|
-
onLog(`! mention scan: gh comments failed for ${prUrl}: ${err
|
|
97577
|
+
onLog(`! mention scan: gh comments failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
|
|
96872
97578
|
return [];
|
|
96873
97579
|
}
|
|
96874
97580
|
}
|
|
97581
|
+
const commentSyncEnabled = Boolean(cfg.linear.syncTasksToComment && apiKey);
|
|
97582
|
+
const commentMutations = {
|
|
97583
|
+
createIssueComment,
|
|
97584
|
+
updateIssueComment,
|
|
97585
|
+
deleteIssueComment
|
|
97586
|
+
};
|
|
96875
97587
|
const coord = new AgentCoordinator({
|
|
96876
97588
|
fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
|
|
96877
97589
|
fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
|
|
@@ -96900,26 +97612,62 @@ PR: ${prUrl}` : ""
|
|
|
96900
97612
|
const json2 = await file2.json();
|
|
96901
97613
|
return json2.iteration ?? 0;
|
|
96902
97614
|
},
|
|
96903
|
-
...
|
|
97615
|
+
...commentSyncEnabled ? {
|
|
96904
97616
|
syncTasks: async (worker, iteration) => {
|
|
96905
97617
|
const root = cwdByChange.get(worker.changeName) ?? projectRoot;
|
|
96906
|
-
const
|
|
96907
|
-
const
|
|
96908
|
-
const
|
|
97618
|
+
const layout = projectLayout(root);
|
|
97619
|
+
const changeDir = layout.changeDir(worker.changeName);
|
|
97620
|
+
const statePath = layout.stateFile(worker.changeName);
|
|
97621
|
+
await postPlanCommentOnce({
|
|
96909
97622
|
apiKey,
|
|
96910
97623
|
issueId: worker.issueId,
|
|
96911
|
-
|
|
96912
|
-
|
|
97624
|
+
statePath,
|
|
97625
|
+
changeDir,
|
|
97626
|
+
changeName: worker.changeName,
|
|
97627
|
+
log: onLog,
|
|
97628
|
+
mutations: commentMutations
|
|
97629
|
+
});
|
|
97630
|
+
await postOrUpdateTasksComment({
|
|
97631
|
+
apiKey,
|
|
97632
|
+
issueId: worker.issueId,
|
|
97633
|
+
statePath,
|
|
97634
|
+
changeDir,
|
|
96913
97635
|
changeName: worker.changeName,
|
|
96914
97636
|
iteration,
|
|
96915
97637
|
log: onLog,
|
|
96916
|
-
|
|
97638
|
+
mutations: commentMutations
|
|
96917
97639
|
});
|
|
96918
|
-
|
|
96919
|
-
|
|
96920
|
-
|
|
96921
|
-
|
|
97640
|
+
},
|
|
97641
|
+
onSteeringAppended: async (changeName, message) => {
|
|
97642
|
+
const root = cwdByChange.get(changeName) ?? projectRoot;
|
|
97643
|
+
const layout = projectLayout(root);
|
|
97644
|
+
const changeDir = layout.changeDir(changeName);
|
|
97645
|
+
const statePath = layout.stateFile(changeName);
|
|
97646
|
+
const issue2 = issueByChange.get(changeName) ?? null;
|
|
97647
|
+
const issueId = issue2?.id ?? null;
|
|
97648
|
+
if (!issueId) {
|
|
97649
|
+
onLog(` comment-sync: no Linear issue cached for ${changeName}; skipping steering refresh`, "gray");
|
|
97650
|
+
return;
|
|
96922
97651
|
}
|
|
97652
|
+
let iteration = 0;
|
|
97653
|
+
try {
|
|
97654
|
+
const f2 = Bun.file(statePath);
|
|
97655
|
+
if (await f2.exists()) {
|
|
97656
|
+
const json2 = await f2.json();
|
|
97657
|
+
iteration = json2.iteration ?? 0;
|
|
97658
|
+
}
|
|
97659
|
+
} catch {}
|
|
97660
|
+
await postSteeringAndRefreshTasks({
|
|
97661
|
+
apiKey,
|
|
97662
|
+
issueId,
|
|
97663
|
+
statePath,
|
|
97664
|
+
changeDir,
|
|
97665
|
+
changeName,
|
|
97666
|
+
iteration,
|
|
97667
|
+
message,
|
|
97668
|
+
log: onLog,
|
|
97669
|
+
mutations: commentMutations
|
|
97670
|
+
});
|
|
96923
97671
|
}
|
|
96924
97672
|
} : {}
|
|
96925
97673
|
}, {
|
|
@@ -96988,7 +97736,7 @@ PR: ${prUrl}` : ""
|
|
|
96988
97736
|
concurrency,
|
|
96989
97737
|
pollInterval,
|
|
96990
97738
|
getWorkerCwd: (changeName) => cwdByChange.get(changeName),
|
|
96991
|
-
syncTasksEnabled:
|
|
97739
|
+
syncTasksEnabled: commentSyncEnabled,
|
|
96992
97740
|
runBaselineGate: runBaselineGateOnce
|
|
96993
97741
|
};
|
|
96994
97742
|
}
|
|
@@ -97017,6 +97765,7 @@ var init_wire = __esm(() => {
|
|
|
97017
97765
|
init_tasks_md();
|
|
97018
97766
|
init_workflow();
|
|
97019
97767
|
init_types2();
|
|
97768
|
+
init_linear();
|
|
97020
97769
|
init_coordinator();
|
|
97021
97770
|
init_scaffold();
|
|
97022
97771
|
init_worktree();
|
|
@@ -97024,7 +97773,7 @@ var init_wire = __esm(() => {
|
|
|
97024
97773
|
init_post_task();
|
|
97025
97774
|
init_gate();
|
|
97026
97775
|
init_workflow();
|
|
97027
|
-
|
|
97776
|
+
init_comment_sync();
|
|
97028
97777
|
GITHUB_PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
|
|
97029
97778
|
bunGitRunner = {
|
|
97030
97779
|
run: async (args, cwd2) => {
|
|
@@ -97143,21 +97892,26 @@ function readSize2() {
|
|
|
97143
97892
|
rows: process.stdout.rows ?? 24
|
|
97144
97893
|
};
|
|
97145
97894
|
}
|
|
97895
|
+
function clearScreenAndScrollback2() {
|
|
97896
|
+
if (process.stdout.isTTY)
|
|
97897
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
97898
|
+
}
|
|
97146
97899
|
function useTerminalSize2() {
|
|
97147
|
-
const
|
|
97148
|
-
|
|
97149
|
-
|
|
97150
|
-
}));
|
|
97900
|
+
const initial2 = import_react59.useRef({ ...readSize2(), resizeKey: 0 });
|
|
97901
|
+
const [size2, setSize] = import_react59.useState(initial2.current);
|
|
97902
|
+
const sizeRef = import_react59.useRef(initial2.current);
|
|
97151
97903
|
import_react59.useEffect(() => {
|
|
97152
97904
|
if (!process.stdout.isTTY)
|
|
97153
97905
|
return;
|
|
97154
97906
|
const onResize = () => {
|
|
97155
97907
|
const { columns, rows } = readSize2();
|
|
97156
|
-
|
|
97157
|
-
|
|
97158
|
-
|
|
97159
|
-
|
|
97160
|
-
}
|
|
97908
|
+
const prev = sizeRef.current;
|
|
97909
|
+
if (prev.columns === columns && prev.rows === rows)
|
|
97910
|
+
return;
|
|
97911
|
+
clearScreenAndScrollback2();
|
|
97912
|
+
const next = { columns, rows, resizeKey: prev.resizeKey + 1 };
|
|
97913
|
+
sizeRef.current = next;
|
|
97914
|
+
setSize(next);
|
|
97161
97915
|
};
|
|
97162
97916
|
process.stdout.on("resize", onResize);
|
|
97163
97917
|
return () => {
|
|
@@ -97347,7 +98101,7 @@ var init_SteeringField = __esm(async () => {
|
|
|
97347
98101
|
});
|
|
97348
98102
|
|
|
97349
98103
|
// apps/agent/src/components/AgentMode.tsx
|
|
97350
|
-
import { join as
|
|
98104
|
+
import { join as join23 } from "path";
|
|
97351
98105
|
async function appendSteeringImpl(changeDir, message) {
|
|
97352
98106
|
await runWithContext(createDefaultContext(), async () => {
|
|
97353
98107
|
appendSteeringMessage(changeDir, message);
|
|
@@ -97598,19 +98352,13 @@ function AgentMode({
|
|
|
97598
98352
|
loadConfig = loadRalphyConfig
|
|
97599
98353
|
}) {
|
|
97600
98354
|
const { exit } = use_app_default();
|
|
97601
|
-
const { stdout } = use_stdout_default();
|
|
97602
98355
|
const { isRawModeSupported } = use_stdin_default();
|
|
97603
98356
|
const { columns, rows, resizeKey } = useTerminalSize2();
|
|
97604
|
-
import_react61.useEffect(() => {
|
|
97605
|
-
if (resizeKey === 0)
|
|
97606
|
-
return;
|
|
97607
|
-
stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
97608
|
-
}, [resizeKey, stdout]);
|
|
97609
98357
|
const [logs, setLogs] = import_react61.useState([]);
|
|
97610
98358
|
const [, setTick] = import_react61.useState(0);
|
|
97611
98359
|
const [clock, setClock] = import_react61.useState(0);
|
|
97612
98360
|
const [focusedIdx, setFocusedIdx] = import_react61.useState(0);
|
|
97613
|
-
const [showPendingTasks, setShowPendingTasks] = import_react61.useState(
|
|
98361
|
+
const [showPendingTasks, setShowPendingTasks] = import_react61.useState(false);
|
|
97614
98362
|
const [showAllSubtasks, setShowAllSubtasks] = import_react61.useState(false);
|
|
97615
98363
|
const coordRef = import_react61.useRef(null);
|
|
97616
98364
|
const workerMetaRef = import_react61.useRef(new Map);
|
|
@@ -97808,7 +98556,7 @@ function AgentMode({
|
|
|
97808
98556
|
(async () => {
|
|
97809
98557
|
for (const [changeName, meta3] of workerMetaRef.current) {
|
|
97810
98558
|
try {
|
|
97811
|
-
const file2 = Bun.file(
|
|
98559
|
+
const file2 = Bun.file(join23(meta3.statesDir, changeName, ".ralph-state.json"));
|
|
97812
98560
|
if (await file2.exists()) {
|
|
97813
98561
|
const json2 = await file2.json();
|
|
97814
98562
|
meta3.iter = json2.iteration ?? meta3.iter;
|
|
@@ -97818,9 +98566,9 @@ function AgentMode({
|
|
|
97818
98566
|
}
|
|
97819
98567
|
if (meta3.changeDir) {
|
|
97820
98568
|
try {
|
|
97821
|
-
const tasksFile = Bun.file(
|
|
97822
|
-
const proposalFile = Bun.file(
|
|
97823
|
-
const designFile = Bun.file(
|
|
98569
|
+
const tasksFile = Bun.file(join23(meta3.changeDir, "tasks.md"));
|
|
98570
|
+
const proposalFile = Bun.file(join23(meta3.changeDir, "proposal.md"));
|
|
98571
|
+
const designFile = Bun.file(join23(meta3.changeDir, "design.md"));
|
|
97824
98572
|
const [tasksText, proposalText, designText] = await Promise.all([
|
|
97825
98573
|
tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
|
|
97826
98574
|
proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
|
|
@@ -98651,11 +99399,14 @@ function AgentMode({
|
|
|
98651
99399
|
},
|
|
98652
99400
|
onSubmit: async (message) => {
|
|
98653
99401
|
try {
|
|
98654
|
-
await appendSteering(
|
|
99402
|
+
await appendSteering(join23(tasksDir, w.changeName), message);
|
|
98655
99403
|
} catch (err) {
|
|
98656
99404
|
appendLog(`! steering append failed for ${w.changeName}: ${err.message}`, "red");
|
|
98657
99405
|
throw err;
|
|
98658
99406
|
}
|
|
99407
|
+
try {
|
|
99408
|
+
await coordRef.current?.notifySteeringAppended?.(w.changeName, message);
|
|
99409
|
+
} catch {}
|
|
98659
99410
|
const restarted = await coordRef.current?.restartWorker(w.changeName);
|
|
98660
99411
|
if (restarted) {
|
|
98661
99412
|
appendLog(` ${w.changeName}: steering applied, restarting worker`, "cyan");
|
|
@@ -98774,7 +99525,7 @@ function bucketChecks(rollup, prState) {
|
|
|
98774
99525
|
return "fail";
|
|
98775
99526
|
return "pass";
|
|
98776
99527
|
}
|
|
98777
|
-
async function fetchPrStatus(url2, runner, cwd2) {
|
|
99528
|
+
async function fetchPrStatus(url2, runner, cwd2, transition) {
|
|
98778
99529
|
let stdout;
|
|
98779
99530
|
try {
|
|
98780
99531
|
const out = await runner.run(["gh", "pr", "view", url2, "--json", PR_VIEW_FIELDS], cwd2);
|
|
@@ -98795,6 +99546,9 @@ async function fetchPrStatus(url2, runner, cwd2) {
|
|
|
98795
99546
|
const state = stateUpper === "OPEN" || stateUpper === "CLOSED" || stateUpper === "MERGED" ? stateUpper : "OPEN";
|
|
98796
99547
|
const mergeableUpper = (raw.mergeable ?? "UNKNOWN").toUpperCase();
|
|
98797
99548
|
const mergeable = mergeableUpper === "MERGEABLE" || mergeableUpper === "CONFLICTING" ? mergeableUpper : "UNKNOWN";
|
|
99549
|
+
if (transition && transition.priorState !== state) {
|
|
99550
|
+
transition.onTransition(state);
|
|
99551
|
+
}
|
|
98798
99552
|
return {
|
|
98799
99553
|
kind: "ok",
|
|
98800
99554
|
state,
|
|
@@ -98855,7 +99609,7 @@ var exports_list = {};
|
|
|
98855
99609
|
__export(exports_list, {
|
|
98856
99610
|
runList: () => runList
|
|
98857
99611
|
});
|
|
98858
|
-
import { join as
|
|
99612
|
+
import { join as join24 } from "path";
|
|
98859
99613
|
function countTaskItems(content) {
|
|
98860
99614
|
const checked = (content.match(/^- \[x\]/gm) ?? []).length;
|
|
98861
99615
|
const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
|
|
@@ -98868,13 +99622,13 @@ function buildLocalRows(statesDir, projectRoot) {
|
|
|
98868
99622
|
const sources = [{ dir: statesDir, label: "main" }];
|
|
98869
99623
|
const worktreesRoot = worktreesDir2(projectRoot);
|
|
98870
99624
|
for (const wt of storage.list(worktreesRoot)) {
|
|
98871
|
-
sources.push({ dir:
|
|
99625
|
+
sources.push({ dir: join24(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
|
|
98872
99626
|
}
|
|
98873
99627
|
for (const { dir, label } of sources) {
|
|
98874
99628
|
for (const entry of storage.list(dir)) {
|
|
98875
99629
|
if (seen.has(entry))
|
|
98876
99630
|
continue;
|
|
98877
|
-
const raw = storage.read(
|
|
99631
|
+
const raw = storage.read(join24(dir, entry, ".ralph-state.json"));
|
|
98878
99632
|
if (raw === null)
|
|
98879
99633
|
continue;
|
|
98880
99634
|
let state;
|
|
@@ -98889,7 +99643,7 @@ function buildLocalRows(statesDir, projectRoot) {
|
|
|
98889
99643
|
const firstLine = promptRaw.split(`
|
|
98890
99644
|
`).find((l) => l.trim() !== "") ?? "";
|
|
98891
99645
|
let progress = "\u2014";
|
|
98892
|
-
const tasksContent = storage.read(
|
|
99646
|
+
const tasksContent = storage.read(join24(dir, entry, "tasks.md"));
|
|
98893
99647
|
if (tasksContent !== null) {
|
|
98894
99648
|
const { checked, unchecked } = countTaskItems(tasksContent);
|
|
98895
99649
|
const total = checked + unchecked;
|
|
@@ -99048,10 +99802,20 @@ ${bucket.label}: error fetching from Linear \u2014 ${error48}
|
|
|
99048
99802
|
}
|
|
99049
99803
|
}
|
|
99050
99804
|
const rows = [...seen.values()];
|
|
99805
|
+
try {
|
|
99806
|
+
const attachmentsByIssue = await fetchAttachmentsForIssues(apiKey, rows.map((r) => r.issueId));
|
|
99807
|
+
for (const row of rows) {
|
|
99808
|
+
const attachments = attachmentsByIssue.get(row.issueId) ?? [];
|
|
99809
|
+
row.prUrl = findPullRequestUrl(attachments);
|
|
99810
|
+
}
|
|
99811
|
+
} catch {}
|
|
99051
99812
|
await Promise.all(rows.map(async (row) => {
|
|
99813
|
+
if (row.prUrl)
|
|
99814
|
+
return;
|
|
99052
99815
|
try {
|
|
99053
|
-
const
|
|
99054
|
-
|
|
99816
|
+
const fromGitHub = await discoverPrUrlFromGitHub(row.identifier, runner, cwd2);
|
|
99817
|
+
if (fromGitHub)
|
|
99818
|
+
row.prUrl = fromGitHub;
|
|
99055
99819
|
} catch {}
|
|
99056
99820
|
}));
|
|
99057
99821
|
await Promise.all(rows.map(async (row) => {
|
|
@@ -99283,6 +100047,7 @@ var init_list = __esm(() => {
|
|
|
99283
100047
|
init_types2();
|
|
99284
100048
|
init_worktree();
|
|
99285
100049
|
init_config();
|
|
100050
|
+
init_linear();
|
|
99286
100051
|
init_list_sort();
|
|
99287
100052
|
localCmdRunner = {
|
|
99288
100053
|
run: async (cmd, cwd2) => {
|
|
@@ -99305,8 +100070,8 @@ var exports_json_runner = {};
|
|
|
99305
100070
|
__export(exports_json_runner, {
|
|
99306
100071
|
runAgentJson: () => runAgentJson
|
|
99307
100072
|
});
|
|
99308
|
-
import { join as
|
|
99309
|
-
import { mkdir as
|
|
100073
|
+
import { join as join25 } from "path";
|
|
100074
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
99310
100075
|
import { homedir as homedir5 } from "os";
|
|
99311
100076
|
function cleanOutputLine2(raw) {
|
|
99312
100077
|
const clean = raw.replace(ANSI_STRIP_RE2, "").trim();
|
|
@@ -99330,7 +100095,7 @@ async function runAgentJson({
|
|
|
99330
100095
|
statesDir,
|
|
99331
100096
|
tasksDir
|
|
99332
100097
|
}) {
|
|
99333
|
-
await
|
|
100098
|
+
await mkdir8(join25(homedir5(), ".ralph"), { recursive: true }).catch(() => {
|
|
99334
100099
|
return;
|
|
99335
100100
|
});
|
|
99336
100101
|
const cfgPath = await ensureRalphyConfig(projectRoot);
|
|
@@ -99484,8 +100249,8 @@ var exports_src2 = {};
|
|
|
99484
100249
|
__export(exports_src2, {
|
|
99485
100250
|
main: () => main2
|
|
99486
100251
|
});
|
|
99487
|
-
import { mkdir as
|
|
99488
|
-
import { join as
|
|
100252
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
100253
|
+
import { join as join26 } from "path";
|
|
99489
100254
|
async function main2(argv) {
|
|
99490
100255
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
99491
100256
|
printHelp2();
|
|
@@ -99519,9 +100284,9 @@ async function main2(argv) {
|
|
|
99519
100284
|
});
|
|
99520
100285
|
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
99521
100286
|
}
|
|
99522
|
-
await
|
|
99523
|
-
await
|
|
99524
|
-
await
|
|
100287
|
+
await mkdir9(statesDir, { recursive: true });
|
|
100288
|
+
await mkdir9(tasksDir, { recursive: true });
|
|
100289
|
+
await mkdir9(join26(projectRoot, ".ralph"), { recursive: true });
|
|
99525
100290
|
if (args.jsonOutput) {
|
|
99526
100291
|
const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
|
|
99527
100292
|
await runAgentJson2({ args, projectRoot, statesDir, tasksDir });
|