@neriros/ralphy 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/dist/mcp/index.js +69 -2
- package/dist/shell/index.js +1001 -171
- 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.0")
|
|
18932
|
+
return "3.3.0";
|
|
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(),
|
|
@@ -68802,21 +68860,26 @@ function readSize() {
|
|
|
68802
68860
|
rows: process.stdout.rows ?? 24
|
|
68803
68861
|
};
|
|
68804
68862
|
}
|
|
68863
|
+
function clearScreenAndScrollback() {
|
|
68864
|
+
if (process.stdout.isTTY)
|
|
68865
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
68866
|
+
}
|
|
68805
68867
|
function useTerminalSize() {
|
|
68806
|
-
const
|
|
68807
|
-
|
|
68808
|
-
|
|
68809
|
-
}));
|
|
68868
|
+
const initial2 = import_react53.useRef({ ...readSize(), resizeKey: 0 });
|
|
68869
|
+
const [size2, setSize] = import_react53.useState(initial2.current);
|
|
68870
|
+
const sizeRef = import_react53.useRef(initial2.current);
|
|
68810
68871
|
import_react53.useEffect(() => {
|
|
68811
68872
|
if (!process.stdout.isTTY)
|
|
68812
68873
|
return;
|
|
68813
68874
|
const onResize = () => {
|
|
68814
68875
|
const { columns, rows } = readSize();
|
|
68815
|
-
|
|
68816
|
-
|
|
68817
|
-
|
|
68818
|
-
|
|
68819
|
-
}
|
|
68876
|
+
const prev = sizeRef.current;
|
|
68877
|
+
if (prev.columns === columns && prev.rows === rows)
|
|
68878
|
+
return;
|
|
68879
|
+
clearScreenAndScrollback();
|
|
68880
|
+
const next = { columns, rows, resizeKey: prev.resizeKey + 1 };
|
|
68881
|
+
sizeRef.current = next;
|
|
68882
|
+
setSize(next);
|
|
68820
68883
|
};
|
|
68821
68884
|
process.stdout.on("resize", onResize);
|
|
68822
68885
|
return () => {
|
|
@@ -68864,7 +68927,7 @@ function StatusBar({
|
|
|
68864
68927
|
return () => clearInterval(id);
|
|
68865
68928
|
}, [isRunning, startedAt]);
|
|
68866
68929
|
const { columns } = useTerminalSize();
|
|
68867
|
-
const barWidth = Math.max(8,
|
|
68930
|
+
const barWidth = Math.max(8, columns);
|
|
68868
68931
|
const bar = "\u2500".repeat(barWidth);
|
|
68869
68932
|
return /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Box_default, {
|
|
68870
68933
|
flexDirection: "column",
|
|
@@ -70223,6 +70286,37 @@ ${failureOutput.trim()}
|
|
|
70223
70286
|
${fence}`;
|
|
70224
70287
|
await Bun.write(tasksPath, prependSection(existing, stamped, body));
|
|
70225
70288
|
}
|
|
70289
|
+
function normalizeNewlyAppendedSectionWithReport(previous, current) {
|
|
70290
|
+
const prevHeadings = new Set;
|
|
70291
|
+
for (const line of previous.split(`
|
|
70292
|
+
`)) {
|
|
70293
|
+
if (line.startsWith("## "))
|
|
70294
|
+
prevHeadings.add(line);
|
|
70295
|
+
}
|
|
70296
|
+
const sections = current.split(/(?=^## )/m);
|
|
70297
|
+
const headings = [];
|
|
70298
|
+
let count = 0;
|
|
70299
|
+
const out = sections.map((section) => {
|
|
70300
|
+
const nlIdx = section.indexOf(`
|
|
70301
|
+
`);
|
|
70302
|
+
const headingLine = nlIdx === -1 ? section.replace(/\n$/, "") : section.slice(0, nlIdx);
|
|
70303
|
+
if (!headingLine.startsWith("## "))
|
|
70304
|
+
return section;
|
|
70305
|
+
if (prevHeadings.has(headingLine))
|
|
70306
|
+
return section;
|
|
70307
|
+
let localCount = 0;
|
|
70308
|
+
const rewritten = section.replace(/^(\s*)- \[[xX]\] (.+)$/gm, (_m, indent, rest2) => {
|
|
70309
|
+
localCount += 1;
|
|
70310
|
+
return `${indent}- [ ] ${rest2}`;
|
|
70311
|
+
});
|
|
70312
|
+
if (localCount > 0) {
|
|
70313
|
+
headings.push(headingLine.slice(3));
|
|
70314
|
+
count += localCount;
|
|
70315
|
+
}
|
|
70316
|
+
return rewritten;
|
|
70317
|
+
});
|
|
70318
|
+
return { text: count > 0 ? out.join("") : current, headings, count };
|
|
70319
|
+
}
|
|
70226
70320
|
var MISSION_TASKS_FILENAME = "tasks.md", AGENT_TASKS_FILENAME = "agent-tasks.md", FLOW_TASK_HEADING_PREFIXES;
|
|
70227
70321
|
var init_tasks_md = __esm(() => {
|
|
70228
70322
|
FLOW_TASK_HEADING_PREFIXES = [
|
|
@@ -70832,14 +70926,8 @@ function TaskLoop({ opts }) {
|
|
|
70832
70926
|
const { exit } = use_app_default();
|
|
70833
70927
|
const loop = useLoop(opts);
|
|
70834
70928
|
const { isRawModeSupported } = use_stdin_default();
|
|
70835
|
-
const { stdout } = use_stdout_default();
|
|
70836
70929
|
const { resizeKey } = useTerminalSize();
|
|
70837
70930
|
const bannerItem = import_react56.useRef({ id: "__banner__", kind: "banner" });
|
|
70838
|
-
import_react56.useEffect(() => {
|
|
70839
|
-
if (resizeKey === 0)
|
|
70840
|
-
return;
|
|
70841
|
-
stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
70842
|
-
}, [resizeKey, stdout]);
|
|
70843
70931
|
const feedItems = import_react56.useMemo(() => [
|
|
70844
70932
|
bannerItem.current,
|
|
70845
70933
|
...loop.logLines.map((e) => ({ id: e.id, kind: "entry", entry: e }))
|
|
@@ -92379,7 +92467,7 @@ var init_zod2 = __esm(() => {
|
|
|
92379
92467
|
});
|
|
92380
92468
|
|
|
92381
92469
|
// packages/workflow/src/schema.ts
|
|
92382
|
-
var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, BoundariesSchema, WorkflowConfigSchema;
|
|
92470
|
+
var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
|
|
92383
92471
|
var init_schema = __esm(() => {
|
|
92384
92472
|
init_zod2();
|
|
92385
92473
|
MarkerSchema = exports_external2.object({
|
|
@@ -92390,7 +92478,7 @@ var init_schema = __esm(() => {
|
|
|
92390
92478
|
filter: exports_external2.array(MarkerSchema).default([])
|
|
92391
92479
|
});
|
|
92392
92480
|
SetIndicatorSchema = exports_external2.union([exports_external2.array(MarkerSchema).min(1), MarkerSchema]);
|
|
92393
|
-
IndicatorsSchema = exports_external2.object({
|
|
92481
|
+
IndicatorsSchema = exports_external2.preprocess((v) => v == null ? {} : v, exports_external2.object({
|
|
92394
92482
|
getTodo: GetIndicatorSchema.optional(),
|
|
92395
92483
|
getInProgress: GetIndicatorSchema.optional(),
|
|
92396
92484
|
getConflicted: GetIndicatorSchema.optional(),
|
|
@@ -92419,7 +92507,7 @@ var init_schema = __esm(() => {
|
|
|
92419
92507
|
}
|
|
92420
92508
|
}
|
|
92421
92509
|
}
|
|
92422
|
-
});
|
|
92510
|
+
}));
|
|
92423
92511
|
ProjectSchema = exports_external2.object({
|
|
92424
92512
|
name: exports_external2.string().optional(),
|
|
92425
92513
|
language: exports_external2.string().optional(),
|
|
@@ -92431,9 +92519,17 @@ var init_schema = __esm(() => {
|
|
|
92431
92519
|
build: exports_external2.string().optional(),
|
|
92432
92520
|
typecheck: exports_external2.string().optional()
|
|
92433
92521
|
}).catchall(exports_external2.string()).default({});
|
|
92522
|
+
DEFAULT_META_ONLY_FILES = [
|
|
92523
|
+
"openspec/**",
|
|
92524
|
+
".ralph/**",
|
|
92525
|
+
"**/agent-tasks.md",
|
|
92526
|
+
"**/tasks.md",
|
|
92527
|
+
"**/MANUAL_TESTING*.md"
|
|
92528
|
+
];
|
|
92434
92529
|
BoundariesSchema = exports_external2.object({
|
|
92435
|
-
never_touch: exports_external2.array(exports_external2.string()).default([])
|
|
92436
|
-
|
|
92530
|
+
never_touch: exports_external2.array(exports_external2.string()).default([]),
|
|
92531
|
+
meta_only_files: exports_external2.array(exports_external2.string()).default(DEFAULT_META_ONLY_FILES)
|
|
92532
|
+
}).strict().default({ never_touch: [], meta_only_files: DEFAULT_META_ONLY_FILES });
|
|
92437
92533
|
WorkflowConfigSchema = exports_external2.object({
|
|
92438
92534
|
project: ProjectSchema,
|
|
92439
92535
|
commands: CommandsSchema,
|
|
@@ -92470,18 +92566,20 @@ var init_schema = __esm(() => {
|
|
|
92470
92566
|
assignee: exports_external2.string().optional(),
|
|
92471
92567
|
postComments: exports_external2.boolean().default(true),
|
|
92472
92568
|
updateEveryIterations: exports_external2.number().int().nonnegative().default(10),
|
|
92473
|
-
mentionTrigger: exports_external2.boolean().default(
|
|
92569
|
+
mentionTrigger: exports_external2.boolean().default(true),
|
|
92474
92570
|
mentionHandle: exports_external2.string().default("@ralphy"),
|
|
92475
|
-
codeReviewTrigger: exports_external2.boolean().default(
|
|
92571
|
+
codeReviewTrigger: exports_external2.boolean().default(true),
|
|
92476
92572
|
codeReviewStaleHours: exports_external2.number().nonnegative().default(24),
|
|
92573
|
+
syncTasksToComment: exports_external2.boolean().default(true),
|
|
92477
92574
|
indicators: IndicatorsSchema.default({})
|
|
92478
92575
|
}).strict().default({
|
|
92479
92576
|
postComments: true,
|
|
92480
92577
|
updateEveryIterations: 10,
|
|
92481
|
-
mentionTrigger:
|
|
92578
|
+
mentionTrigger: true,
|
|
92482
92579
|
mentionHandle: "@ralphy",
|
|
92483
|
-
codeReviewTrigger:
|
|
92580
|
+
codeReviewTrigger: true,
|
|
92484
92581
|
codeReviewStaleHours: 24,
|
|
92582
|
+
syncTasksToComment: true,
|
|
92485
92583
|
indicators: {}
|
|
92486
92584
|
}),
|
|
92487
92585
|
github: exports_external2.object({
|
|
@@ -92540,70 +92638,71 @@ boundaries:
|
|
|
92540
92638
|
never_touch:
|
|
92541
92639
|
- "dist/**"
|
|
92542
92640
|
- ".claude/worktrees/**"
|
|
92543
|
-
|
|
92641
|
+
# Files that count as "meta only" for the pre-PR substantive-diff guard.
|
|
92642
|
+
# If every changed file matches one of these globs, the loop refuses to
|
|
92643
|
+
# open the PR and respawns the worker \u2014 the actual implementation was
|
|
92644
|
+
# lost (either deleted mid-loop or absorbed by a merge from base).
|
|
92645
|
+
meta_only_files:
|
|
92646
|
+
- "openspec/**"
|
|
92647
|
+
- ".ralph/**"
|
|
92648
|
+
- "**/agent-tasks.md"
|
|
92649
|
+
- "**/tasks.md"
|
|
92650
|
+
- "**/MANUAL_TESTING*.md"
|
|
92651
|
+
|
|
92652
|
+
# \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
|
|
92544
92653
|
# How many tasks to run in parallel.
|
|
92545
92654
|
concurrency: 1
|
|
92546
|
-
|
|
92547
92655
|
# Seconds between polls for new Linear issues (agent mode).
|
|
92548
92656
|
pollIntervalSeconds: 60
|
|
92657
|
+
# Seconds to wait between loop iterations (throttle).
|
|
92658
|
+
iterationDelaySeconds: 0
|
|
92549
92659
|
|
|
92550
|
-
#
|
|
92660
|
+
# \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
|
|
92551
92661
|
maxIterationsPerTask: 0
|
|
92552
|
-
|
|
92553
|
-
# Maximum cost in USD per task. 0 = unlimited.
|
|
92554
92662
|
maxCostUsdPerTask: 0
|
|
92555
|
-
|
|
92556
|
-
# Maximum wall-clock minutes per task. 0 = unlimited.
|
|
92557
92663
|
maxRuntimeMinutesPerTask: 0
|
|
92558
|
-
|
|
92559
92664
|
# Stop a task after this many consecutive identical failures.
|
|
92560
92665
|
maxConsecutiveFailuresPerTask: 5
|
|
92561
92666
|
|
|
92562
|
-
#
|
|
92563
|
-
|
|
92564
|
-
|
|
92667
|
+
# \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
|
|
92668
|
+
# Underlying engine: "claude" or "codex".
|
|
92669
|
+
engine: claude
|
|
92670
|
+
# Model tier: "haiku", "sonnet", or "opus".
|
|
92671
|
+
model: opus
|
|
92565
92672
|
# Log the raw engine stream to stdout.
|
|
92566
92673
|
logRawStream: false
|
|
92567
|
-
|
|
92568
92674
|
# Pass --verbose to the ralph task sub-process.
|
|
92569
92675
|
taskVerbose: false
|
|
92570
92676
|
|
|
92677
|
+
# \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
|
|
92571
92678
|
# Run each task in an isolated git worktree.
|
|
92572
92679
|
useWorktree: false
|
|
92573
|
-
|
|
92574
92680
|
# Delete the worktree after a successful task.
|
|
92575
92681
|
cleanupWorktreeOnSuccess: false
|
|
92576
92682
|
|
|
92683
|
+
# \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
|
|
92577
92684
|
# Open a pull request after a task succeeds.
|
|
92578
92685
|
createPrOnSuccess: false
|
|
92579
|
-
|
|
92580
92686
|
# Base branch for pull requests.
|
|
92581
92687
|
prBaseBranch: main
|
|
92582
|
-
|
|
92583
92688
|
# When true, stack dependent issues' PRs onto their blocker's open PR.
|
|
92584
92689
|
stackPrsOnDependencies: false
|
|
92585
|
-
|
|
92586
92690
|
# Strategy used when GitHub auto-merge is enabled.
|
|
92587
92691
|
autoMergeStrategy: squash
|
|
92588
92692
|
|
|
92693
|
+
# \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
|
|
92589
92694
|
# Let the agent attempt to fix CI failures after a PR is created.
|
|
92590
92695
|
fixCiOnFailure: false
|
|
92591
|
-
|
|
92592
92696
|
# Maximum number of CI-fix attempts per task.
|
|
92593
92697
|
maxCiFixAttempts: 5
|
|
92594
|
-
|
|
92595
92698
|
# Seconds between CI status polls.
|
|
92596
92699
|
ciPollIntervalSeconds: 30
|
|
92597
92700
|
|
|
92598
|
-
#
|
|
92599
|
-
|
|
92600
|
-
|
|
92601
|
-
#
|
|
92602
|
-
|
|
92603
|
-
|
|
92604
|
-
# Pre-existing error check: gate the agent when the base branch is already broken.
|
|
92605
|
-
# When enabled, the agent runs these commands against the base branch HEAD before
|
|
92606
|
-
# scheduling new work; failures open a Linear ticket and pause new pickups.
|
|
92701
|
+
# \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
|
|
92702
|
+
# Pre-existing error check: gate the agent when the base branch is already
|
|
92703
|
+
# broken. When enabled, the agent runs these commands against the base
|
|
92704
|
+
# branch HEAD before scheduling new work; failures open a Linear ticket
|
|
92705
|
+
# and pause new pickups.
|
|
92607
92706
|
preExistingErrorCheck:
|
|
92608
92707
|
enabled: false
|
|
92609
92708
|
# Commands to run against the base branch. When empty, falls back to commands.lint / commands.test.
|
|
@@ -92612,33 +92711,49 @@ preExistingErrorCheck:
|
|
|
92612
92711
|
label: "ralph:pre-existing-error"
|
|
92613
92712
|
outputCharLimit: 4000
|
|
92614
92713
|
|
|
92714
|
+
# \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
|
|
92615
92715
|
linear:
|
|
92616
92716
|
# Linear team key (e.g. "ENG"). Omit to match all teams.
|
|
92617
92717
|
# team: ENG
|
|
92618
92718
|
|
|
92619
92719
|
# Post progress comments on the Linear issue while a task is running.
|
|
92620
92720
|
postComments: true
|
|
92621
|
-
|
|
92622
92721
|
# Post a progress update every N iterations. 0 disables.
|
|
92623
92722
|
updateEveryIterations: 10
|
|
92624
92723
|
|
|
92625
92724
|
# Watch done-issue comments + linked GitHub PR comments for @ralphy mentions.
|
|
92626
|
-
mentionTrigger:
|
|
92725
|
+
mentionTrigger: true
|
|
92627
92726
|
mentionHandle: "@ralphy"
|
|
92628
92727
|
|
|
92629
92728
|
# Watch open tracked PRs for unresolved review-thread comments.
|
|
92630
|
-
codeReviewTrigger:
|
|
92729
|
+
codeReviewTrigger: true
|
|
92631
92730
|
codeReviewStaleHours: 24
|
|
92632
92731
|
|
|
92732
|
+
# Mirror the loop's tasks.md into a sticky Linear comment (always the
|
|
92733
|
+
# last comment on the issue). Updates on worker launch, on the same
|
|
92734
|
+
# cadence as updateEveryIterations, and on done-transition.
|
|
92735
|
+
syncTasksToComment: true
|
|
92736
|
+
|
|
92633
92737
|
# Indicators map Ralph lifecycle events to Linear labels/statuses.
|
|
92634
|
-
#
|
|
92635
|
-
#
|
|
92636
|
-
|
|
92637
|
-
|
|
92738
|
+
#
|
|
92739
|
+
# Filter semantics (per indicator's \`filter:\` list):
|
|
92740
|
+
# \u2022 Entries of the SAME type (e.g. two \`status\` entries) are ORed
|
|
92741
|
+
# \u2014 the issue matches if any value matches.
|
|
92742
|
+
# \u2022 Entries of DIFFERENT types (one \`status\` + one \`label\`) are
|
|
92743
|
+
# ANDed \u2014 the issue must satisfy every type.
|
|
92744
|
+
# Example: a filter with two statuses + one label matches issues
|
|
92745
|
+
# where status \u2208 {A, B} AND label = L.
|
|
92746
|
+
#
|
|
92747
|
+
# Sections below group one state at a time; its get/set/clear sit
|
|
92748
|
+
# adjacent so the lifecycle reads top-to-bottom.
|
|
92749
|
+
indicators:
|
|
92750
|
+
# \u2500\u2500 Todo (pickup trigger) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92638
92751
|
# getTodo:
|
|
92639
92752
|
# filter:
|
|
92640
92753
|
# - type: status
|
|
92641
92754
|
# value: Todo
|
|
92755
|
+
#
|
|
92756
|
+
# \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
|
|
92642
92757
|
# getInProgress:
|
|
92643
92758
|
# filter:
|
|
92644
92759
|
# - type: status
|
|
@@ -92647,7 +92762,7 @@ linear:
|
|
|
92647
92762
|
# type: status
|
|
92648
92763
|
# value: In Progress
|
|
92649
92764
|
#
|
|
92650
|
-
#
|
|
92765
|
+
# \u2500\u2500 Done \u2192 Review hand-off \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92651
92766
|
# setDone:
|
|
92652
92767
|
# type: status
|
|
92653
92768
|
# value: In Review
|
|
@@ -92659,7 +92774,7 @@ linear:
|
|
|
92659
92774
|
# type: label
|
|
92660
92775
|
# value: "ralph:review"
|
|
92661
92776
|
#
|
|
92662
|
-
#
|
|
92777
|
+
# \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
|
|
92663
92778
|
# getConflicted:
|
|
92664
92779
|
# filter:
|
|
92665
92780
|
# - type: label
|
|
@@ -92671,13 +92786,13 @@ linear:
|
|
|
92671
92786
|
# type: label
|
|
92672
92787
|
# value: "ralph:conflict"
|
|
92673
92788
|
#
|
|
92674
|
-
#
|
|
92789
|
+
# \u2500\u2500 Auto-merge (opt-in) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92675
92790
|
# getAutoMerge:
|
|
92676
92791
|
# filter:
|
|
92677
92792
|
# - type: label
|
|
92678
92793
|
# value: "ralph:auto-merge"
|
|
92679
92794
|
#
|
|
92680
|
-
#
|
|
92795
|
+
# \u2500\u2500 Error quarantine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
92681
92796
|
# setError:
|
|
92682
92797
|
# type: label
|
|
92683
92798
|
# value: "ralph:error"
|
|
@@ -93574,6 +93689,51 @@ function buildIssueFilter(spec) {
|
|
|
93574
93689
|
}
|
|
93575
93690
|
return where;
|
|
93576
93691
|
}
|
|
93692
|
+
async function fetchMentionScanIssues(apiKey, spec) {
|
|
93693
|
+
const where = {
|
|
93694
|
+
state: { type: { in: ["unstarted", "started", "backlog", "triage", "completed"] } }
|
|
93695
|
+
};
|
|
93696
|
+
if (spec.team)
|
|
93697
|
+
where.team = { key: { eq: spec.team } };
|
|
93698
|
+
if (spec.assignee) {
|
|
93699
|
+
if (spec.assignee === "me")
|
|
93700
|
+
where.assignee = { isMe: { eq: true } };
|
|
93701
|
+
else if (spec.assignee.includes("@"))
|
|
93702
|
+
where.assignee = { email: { eq: spec.assignee } };
|
|
93703
|
+
else
|
|
93704
|
+
where.assignee = { id: { eq: spec.assignee } };
|
|
93705
|
+
}
|
|
93706
|
+
const query = `query MentionScanIssues($filter: IssueFilter) {
|
|
93707
|
+
issues(filter: $filter, first: 50) {
|
|
93708
|
+
nodes {
|
|
93709
|
+
id identifier title description url priority createdAt
|
|
93710
|
+
state { name type }
|
|
93711
|
+
assignee { id email name }
|
|
93712
|
+
labels { nodes { name } }
|
|
93713
|
+
relations(first: 50) {
|
|
93714
|
+
nodes { type relatedIssue { id state { type } } }
|
|
93715
|
+
}
|
|
93716
|
+
}
|
|
93717
|
+
}
|
|
93718
|
+
}`;
|
|
93719
|
+
const data = await linearRequest(apiKey, query, {
|
|
93720
|
+
filter: where
|
|
93721
|
+
});
|
|
93722
|
+
const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
|
|
93723
|
+
return data.issues.nodes.map((n) => ({
|
|
93724
|
+
id: n.id,
|
|
93725
|
+
identifier: n.identifier,
|
|
93726
|
+
title: n.title,
|
|
93727
|
+
description: n.description,
|
|
93728
|
+
url: n.url,
|
|
93729
|
+
state: n.state,
|
|
93730
|
+
assignee: n.assignee,
|
|
93731
|
+
labels: n.labels.nodes.map((l) => l.name),
|
|
93732
|
+
priority: n.priority,
|
|
93733
|
+
createdAt: n.createdAt ?? "",
|
|
93734
|
+
blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
|
|
93735
|
+
}));
|
|
93736
|
+
}
|
|
93577
93737
|
async function fetchOpenIssues(apiKey, spec) {
|
|
93578
93738
|
const where = buildIssueFilter(spec);
|
|
93579
93739
|
const query = `query Issues($filter: IssueFilter) {
|
|
@@ -93610,28 +93770,102 @@ async function fetchOpenIssues(apiKey, spec) {
|
|
|
93610
93770
|
blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
|
|
93611
93771
|
}));
|
|
93612
93772
|
}
|
|
93773
|
+
function isRetryableStatus(status) {
|
|
93774
|
+
return status >= 500 && status <= 599;
|
|
93775
|
+
}
|
|
93776
|
+
function parseRetryAfter(header) {
|
|
93777
|
+
if (!header)
|
|
93778
|
+
return;
|
|
93779
|
+
const trimmed = header.trim();
|
|
93780
|
+
if (!trimmed)
|
|
93781
|
+
return;
|
|
93782
|
+
const asNum = Number(trimmed);
|
|
93783
|
+
if (Number.isFinite(asNum))
|
|
93784
|
+
return Math.max(0, asNum * 1000);
|
|
93785
|
+
const asDate = Date.parse(trimmed);
|
|
93786
|
+
if (Number.isFinite(asDate))
|
|
93787
|
+
return Math.max(0, asDate - Date.now());
|
|
93788
|
+
return;
|
|
93789
|
+
}
|
|
93790
|
+
function backoffMs(attempt2) {
|
|
93791
|
+
const base2 = 250 * 2 ** (attempt2 - 1);
|
|
93792
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
93793
|
+
return base2 + jitter;
|
|
93794
|
+
}
|
|
93795
|
+
function isRateLimitedBody(body) {
|
|
93796
|
+
if (typeof body !== "string" || body.length === 0)
|
|
93797
|
+
return false;
|
|
93798
|
+
return body.toLowerCase().includes("rate limit exceeded");
|
|
93799
|
+
}
|
|
93800
|
+
function isRateLimitedError(err) {
|
|
93801
|
+
if (err === null || typeof err !== "object")
|
|
93802
|
+
return false;
|
|
93803
|
+
return err.rateLimited === true;
|
|
93804
|
+
}
|
|
93805
|
+
function formatLinearError(err) {
|
|
93806
|
+
if (err === null || err === undefined)
|
|
93807
|
+
return String(err);
|
|
93808
|
+
if (typeof err !== "object")
|
|
93809
|
+
return String(err);
|
|
93810
|
+
const e = err;
|
|
93811
|
+
const parts = [];
|
|
93812
|
+
if (e.rateLimited)
|
|
93813
|
+
parts.push("rate limited");
|
|
93814
|
+
if (typeof e.status === "number")
|
|
93815
|
+
parts.push(`HTTP ${e.status}`);
|
|
93816
|
+
if (Array.isArray(e.messages) && e.messages.length > 0) {
|
|
93817
|
+
parts.push(`graphql: ${e.messages.join("; ")}`);
|
|
93818
|
+
}
|
|
93819
|
+
if (typeof e.body === "string" && e.body.length > 0 && !e.rateLimited) {
|
|
93820
|
+
const truncated = e.body.length > 200 ? `${e.body.slice(0, 200)}\u2026` : e.body;
|
|
93821
|
+
parts.push(`body: ${truncated}`);
|
|
93822
|
+
}
|
|
93823
|
+
if (parts.length === 0) {
|
|
93824
|
+
if (typeof e.message === "string" && e.message)
|
|
93825
|
+
return e.message;
|
|
93826
|
+
return String(err);
|
|
93827
|
+
}
|
|
93828
|
+
if (typeof e.message === "string" && e.message && !e.rateLimited)
|
|
93829
|
+
parts.unshift(e.message);
|
|
93830
|
+
return parts.join(" \u2014 ");
|
|
93831
|
+
}
|
|
93613
93832
|
async function linearRequest(apiKey, query, variables) {
|
|
93614
|
-
|
|
93615
|
-
|
|
93616
|
-
|
|
93617
|
-
|
|
93618
|
-
|
|
93619
|
-
|
|
93620
|
-
|
|
93621
|
-
|
|
93622
|
-
|
|
93623
|
-
|
|
93624
|
-
|
|
93625
|
-
|
|
93626
|
-
|
|
93627
|
-
|
|
93628
|
-
|
|
93629
|
-
|
|
93630
|
-
|
|
93631
|
-
|
|
93632
|
-
|
|
93833
|
+
let lastHttpError;
|
|
93834
|
+
for (let attempt2 = 1;attempt2 <= MAX_LINEAR_ATTEMPTS; attempt2++) {
|
|
93835
|
+
const res = await fetch(LINEAR_API, {
|
|
93836
|
+
method: "POST",
|
|
93837
|
+
headers: { "Content-Type": "application/json", Authorization: apiKey },
|
|
93838
|
+
body: JSON.stringify({ query, variables })
|
|
93839
|
+
});
|
|
93840
|
+
if (!res.ok) {
|
|
93841
|
+
const err = new Error("Linear API request failed");
|
|
93842
|
+
err.status = res.status;
|
|
93843
|
+
err.body = await res.text();
|
|
93844
|
+
if (res.status === 429 || isRateLimitedBody(err.body)) {
|
|
93845
|
+
err.rateLimited = true;
|
|
93846
|
+
throw err;
|
|
93847
|
+
}
|
|
93848
|
+
lastHttpError = err;
|
|
93849
|
+
if (isRetryableStatus(res.status) && attempt2 < MAX_LINEAR_ATTEMPTS) {
|
|
93850
|
+
const ra = parseRetryAfter(res.headers.get("Retry-After"));
|
|
93851
|
+
const waitMs = Math.min(ra ?? backoffMs(attempt2), MAX_RETRY_AFTER_MS);
|
|
93852
|
+
await linearRequestInternals.sleep(waitMs);
|
|
93853
|
+
continue;
|
|
93854
|
+
}
|
|
93855
|
+
throw err;
|
|
93856
|
+
}
|
|
93857
|
+
const json2 = await res.json();
|
|
93858
|
+
if (json2.errors?.length) {
|
|
93859
|
+
const err = new Error("Linear API returned errors");
|
|
93860
|
+
err.messages = json2.errors.map((e) => e.message);
|
|
93861
|
+
throw err;
|
|
93862
|
+
}
|
|
93863
|
+
if (!json2.data) {
|
|
93864
|
+
throw new Error("Linear API returned no data");
|
|
93865
|
+
}
|
|
93866
|
+
return json2.data;
|
|
93633
93867
|
}
|
|
93634
|
-
|
|
93868
|
+
throw lastHttpError ?? new Error("Linear API request failed");
|
|
93635
93869
|
}
|
|
93636
93870
|
async function addReactionToComment(apiKey, commentId, emoji3) {
|
|
93637
93871
|
const mutation = `mutation Reaction($commentId: String!, $emoji: String!) {
|
|
@@ -93651,6 +93885,36 @@ async function addIssueComment(apiKey, issueId, body) {
|
|
|
93651
93885
|
body
|
|
93652
93886
|
});
|
|
93653
93887
|
}
|
|
93888
|
+
async function createIssueComment(apiKey, issueId, body) {
|
|
93889
|
+
const mutation = `mutation Comment($issueId: String!, $body: String!) {
|
|
93890
|
+
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
93891
|
+
success
|
|
93892
|
+
comment { id }
|
|
93893
|
+
}
|
|
93894
|
+
}`;
|
|
93895
|
+
const data = await linearRequest(apiKey, mutation, { issueId, body });
|
|
93896
|
+
const id = data.commentCreate.comment?.id;
|
|
93897
|
+
if (!id)
|
|
93898
|
+
throw new Error("commentCreate returned no comment id");
|
|
93899
|
+
return id;
|
|
93900
|
+
}
|
|
93901
|
+
async function updateIssueComment(apiKey, commentId, body) {
|
|
93902
|
+
const mutation = `mutation UpdateComment($id: String!, $body: String!) {
|
|
93903
|
+
commentUpdate(id: $id, input: { body: $body }) { success }
|
|
93904
|
+
}`;
|
|
93905
|
+
await linearRequest(apiKey, mutation, {
|
|
93906
|
+
id: commentId,
|
|
93907
|
+
body
|
|
93908
|
+
});
|
|
93909
|
+
}
|
|
93910
|
+
async function deleteIssueComment(apiKey, commentId) {
|
|
93911
|
+
const mutation = `mutation DeleteComment($id: String!) {
|
|
93912
|
+
commentDelete(id: $id) { success }
|
|
93913
|
+
}`;
|
|
93914
|
+
await linearRequest(apiKey, mutation, {
|
|
93915
|
+
id: commentId
|
|
93916
|
+
});
|
|
93917
|
+
}
|
|
93654
93918
|
async function fetchIssueComments(apiKey, issueId) {
|
|
93655
93919
|
const query = `query Comments($id: String!) {
|
|
93656
93920
|
issue(id: $id) {
|
|
@@ -93873,7 +94137,12 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
|
|
|
93873
94137
|
labelId
|
|
93874
94138
|
});
|
|
93875
94139
|
}
|
|
93876
|
-
var LINEAR_API = "https://api.linear.app/graphql", RALPHY_ATTACHMENT_TITLE_FILTER = "Ralphy", RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
|
|
94140
|
+
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:";
|
|
94141
|
+
var init_linear = __esm(() => {
|
|
94142
|
+
linearRequestInternals = {
|
|
94143
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
94144
|
+
};
|
|
94145
|
+
});
|
|
93877
94146
|
|
|
93878
94147
|
// apps/agent/src/sort/compare.ts
|
|
93879
94148
|
function chain(...comparators) {
|
|
@@ -93904,6 +94173,7 @@ function compareQueueEntries(getAutoMerge) {
|
|
|
93904
94173
|
}
|
|
93905
94174
|
var MODE_RANK;
|
|
93906
94175
|
var init_queue_order = __esm(() => {
|
|
94176
|
+
init_linear();
|
|
93907
94177
|
MODE_RANK = {
|
|
93908
94178
|
resume: 0,
|
|
93909
94179
|
"conflict-fix": 1,
|
|
@@ -94117,6 +94387,13 @@ class AgentCoordinator {
|
|
|
94117
94387
|
} catch (err) {
|
|
94118
94388
|
this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
|
|
94119
94389
|
}
|
|
94390
|
+
if (this.deps.syncTasks) {
|
|
94391
|
+
try {
|
|
94392
|
+
await this.deps.syncTasks(w, count);
|
|
94393
|
+
} catch (err) {
|
|
94394
|
+
this.deps.onLog(`! sync-tasks (progress) failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
|
|
94395
|
+
}
|
|
94396
|
+
}
|
|
94120
94397
|
}
|
|
94121
94398
|
}
|
|
94122
94399
|
async scanDoneForConflicts() {
|
|
@@ -94289,6 +94566,13 @@ class AgentCoordinator {
|
|
|
94289
94566
|
issue_identifier: issue2.identifier
|
|
94290
94567
|
});
|
|
94291
94568
|
this.deps.onWorkersChanged();
|
|
94569
|
+
if (this.deps.syncTasks) {
|
|
94570
|
+
try {
|
|
94571
|
+
await this.deps.syncTasks(worker, 0);
|
|
94572
|
+
} catch (err) {
|
|
94573
|
+
this.deps.onLog(`! sync-tasks (launch) failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
94574
|
+
}
|
|
94575
|
+
}
|
|
94292
94576
|
handle.exited.then(async (code) => {
|
|
94293
94577
|
const idx = this.workers.indexOf(worker);
|
|
94294
94578
|
if (idx >= 0)
|
|
@@ -94331,8 +94615,42 @@ class AgentCoordinator {
|
|
|
94331
94615
|
} catch {}
|
|
94332
94616
|
return true;
|
|
94333
94617
|
}
|
|
94618
|
+
async notifySteeringAppended(changeName, message) {
|
|
94619
|
+
if (!this.deps.onSteeringAppended)
|
|
94620
|
+
return;
|
|
94621
|
+
try {
|
|
94622
|
+
await this.deps.onSteeringAppended(changeName, message);
|
|
94623
|
+
} catch (err) {
|
|
94624
|
+
this.deps.onLog(`! onSteeringAppended failed for ${changeName}: ${err.message}`, "yellow");
|
|
94625
|
+
}
|
|
94626
|
+
}
|
|
94334
94627
|
async notifyExited(issue2, changeName, code, mode) {
|
|
94335
94628
|
const ok = code === 0;
|
|
94629
|
+
if (this.deps.syncTasks && ok) {
|
|
94630
|
+
const synthetic = {
|
|
94631
|
+
changeName,
|
|
94632
|
+
issueId: issue2.id,
|
|
94633
|
+
issueIdentifier: issue2.identifier,
|
|
94634
|
+
issue: issue2,
|
|
94635
|
+
mode,
|
|
94636
|
+
kill: () => {},
|
|
94637
|
+
lastReportedIteration: 0,
|
|
94638
|
+
restarting: false
|
|
94639
|
+
};
|
|
94640
|
+
try {
|
|
94641
|
+
let iteration = 0;
|
|
94642
|
+
if (this.deps.getIterationCount) {
|
|
94643
|
+
try {
|
|
94644
|
+
iteration = await this.deps.getIterationCount(changeName);
|
|
94645
|
+
} catch {
|
|
94646
|
+
iteration = 0;
|
|
94647
|
+
}
|
|
94648
|
+
}
|
|
94649
|
+
await this.deps.syncTasks(synthetic, iteration);
|
|
94650
|
+
} catch (err) {
|
|
94651
|
+
this.deps.onLog(`! sync-tasks (done) failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
94652
|
+
}
|
|
94653
|
+
}
|
|
94336
94654
|
if (this.opts.postComments !== false) {
|
|
94337
94655
|
const body = ok ? mode === "conflict-fix" ? `\u2705 Ralph resolved merge conflicts on this issue. Change: \`${changeName}\`` : `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\`
|
|
94338
94656
|
|
|
@@ -94415,6 +94733,7 @@ var emptyPrStatus = () => ({ mergeable: 0, conflicted: 0, ciFailed: 0 }), emptyP
|
|
|
94415
94733
|
prStatus: emptyPrStatus()
|
|
94416
94734
|
});
|
|
94417
94735
|
var init_coordinator = __esm(() => {
|
|
94736
|
+
init_linear();
|
|
94418
94737
|
init_queue_order();
|
|
94419
94738
|
init_src();
|
|
94420
94739
|
});
|
|
@@ -94483,7 +94802,7 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = []
|
|
|
94483
94802
|
`- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
|
|
94484
94803
|
`- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
|
|
94485
94804
|
`- [ ] Fill in design.md with the technical design (files to touch, data flow, edge cases)`,
|
|
94486
|
-
`- [ ] Append an \`## Implementation\` section below with concrete mission-specific tasks derived from the plan
|
|
94805
|
+
`- [ ] Append an \`## Implementation\` section below with concrete mission-specific tasks derived from the plan, including tests and \`bun run lint\` / \`bun run test\`. Every item in the new section MUST start as \`- [ ]\` (unchecked) \u2014 do not pre-check items even if you already did the work during planning. The loop ticks them off in later iterations after each one is verified.`,
|
|
94487
94806
|
""
|
|
94488
94807
|
].join(`
|
|
94489
94808
|
`);
|
|
@@ -94783,11 +95102,49 @@ ${issue2.description.trim()}` : ""
|
|
|
94783
95102
|
].filter(Boolean).join(`
|
|
94784
95103
|
`);
|
|
94785
95104
|
}
|
|
95105
|
+
async function diffFilesAgainstBase(runner, cwd2, base2) {
|
|
95106
|
+
let raw = "";
|
|
95107
|
+
try {
|
|
95108
|
+
const r = await runner.run(["git", "diff", "--name-only", `origin/${base2}...HEAD`], cwd2);
|
|
95109
|
+
raw = r.stdout;
|
|
95110
|
+
} catch {
|
|
95111
|
+
try {
|
|
95112
|
+
const r = await runner.run(["git", "diff", "--name-only", `${base2}...HEAD`], cwd2);
|
|
95113
|
+
raw = r.stdout;
|
|
95114
|
+
} catch {
|
|
95115
|
+
return [];
|
|
95116
|
+
}
|
|
95117
|
+
}
|
|
95118
|
+
return raw.split(`
|
|
95119
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
95120
|
+
}
|
|
95121
|
+
async function classifyDiffAgainstMeta(runner, cwd2, base2, metaOnlyFiles) {
|
|
95122
|
+
const files = await diffFilesAgainstBase(runner, cwd2, base2);
|
|
95123
|
+
if (files.length === 0 || metaOnlyFiles.length === 0) {
|
|
95124
|
+
return { files, onlyMeta: false };
|
|
95125
|
+
}
|
|
95126
|
+
const violations = findBoundaryViolations(files, metaOnlyFiles);
|
|
95127
|
+
const metaSet = new Set(violations.map((v) => v.file));
|
|
95128
|
+
const onlyMeta = files.every((f2) => metaSet.has(f2.replace(/\\/g, "/")));
|
|
95129
|
+
return { files, onlyMeta };
|
|
95130
|
+
}
|
|
94786
95131
|
async function createPullRequest(input, runner) {
|
|
94787
95132
|
const base2 = input.base ?? "main";
|
|
94788
95133
|
const log2 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
|
|
94789
95134
|
if (log2.stdout.trim() === "")
|
|
94790
95135
|
return null;
|
|
95136
|
+
const metaOnlyFiles = input.metaOnlyFiles ?? [];
|
|
95137
|
+
if (metaOnlyFiles.length > 0) {
|
|
95138
|
+
const classification = await classifyDiffAgainstMeta(runner, input.cwd, base2, metaOnlyFiles);
|
|
95139
|
+
if (classification.onlyMeta && classification.files.length > 0) {
|
|
95140
|
+
return {
|
|
95141
|
+
url: null,
|
|
95142
|
+
created: false,
|
|
95143
|
+
blocked: "only-meta",
|
|
95144
|
+
blockedFiles: classification.files
|
|
95145
|
+
};
|
|
95146
|
+
}
|
|
95147
|
+
}
|
|
94791
95148
|
await runner.run(["git", "push", "-u", "origin", input.branch], input.cwd);
|
|
94792
95149
|
const existing = await runner.run([
|
|
94793
95150
|
"gh",
|
|
@@ -94812,6 +95169,7 @@ async function createPullRequest(input, runner) {
|
|
|
94812
95169
|
`).pop() ?? "";
|
|
94813
95170
|
return { url: url2, created: true };
|
|
94814
95171
|
}
|
|
95172
|
+
var init_pr = () => {};
|
|
94815
95173
|
|
|
94816
95174
|
// apps/agent/src/agent/post-task.ts
|
|
94817
95175
|
import { join as join20 } from "path";
|
|
@@ -94924,7 +95282,13 @@ async function createPrWithRetry(ctx, issue2) {
|
|
|
94924
95282
|
while (true) {
|
|
94925
95283
|
try {
|
|
94926
95284
|
ctx.emit("pr-create", "git push + gh pr create");
|
|
94927
|
-
pr = await createPullRequest({
|
|
95285
|
+
pr = await createPullRequest({
|
|
95286
|
+
cwd: ctx.cwd,
|
|
95287
|
+
branch: ctx.branch,
|
|
95288
|
+
issue: issue2,
|
|
95289
|
+
base: base2,
|
|
95290
|
+
metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? []
|
|
95291
|
+
}, ctx.cmd);
|
|
94928
95292
|
return { pr, gaveUp: false };
|
|
94929
95293
|
} catch (err) {
|
|
94930
95294
|
const e = err;
|
|
@@ -95190,49 +95554,97 @@ ${indented}${suffix}`, "yellow");
|
|
|
95190
95554
|
}
|
|
95191
95555
|
return PR_FAILED_EXIT;
|
|
95192
95556
|
}
|
|
95193
|
-
const
|
|
95194
|
-
|
|
95195
|
-
|
|
95557
|
+
const maxOuterAttempts = cfg.maxCiFixAttempts;
|
|
95558
|
+
let onlyMetaAttempts = 0;
|
|
95559
|
+
let pr = null;
|
|
95560
|
+
while (true) {
|
|
95561
|
+
const attempt2 = await createPrWithRetry(ctx, issue2);
|
|
95562
|
+
if (attempt2.gaveUp)
|
|
95563
|
+
return PR_FAILED_EXIT;
|
|
95564
|
+
if (attempt2.pr?.blocked === "only-meta") {
|
|
95565
|
+
onlyMetaAttempts += 1;
|
|
95566
|
+
const files = attempt2.pr.blockedFiles ?? [];
|
|
95567
|
+
emit("pr-only-meta", `${files.length} meta file(s)`);
|
|
95568
|
+
log2(`! ${changeName}: branch diff against ${base2} contains only meta files \u2014 implementation appears lost. Refusing to open PR.`, "red");
|
|
95569
|
+
for (const f2 of files)
|
|
95570
|
+
log2(` ${f2}`, "red");
|
|
95571
|
+
if (onlyMetaAttempts > maxOuterAttempts) {
|
|
95572
|
+
log2(`! exceeded ${maxOuterAttempts} only-meta recovery attempts for ${changeName} \u2014 giving up`, "red");
|
|
95573
|
+
return PR_FAILED_EXIT;
|
|
95574
|
+
}
|
|
95575
|
+
const fileList = files.length > 0 ? files.map((f2) => `- ${f2}`).join(`
|
|
95576
|
+
`) : "(empty diff)";
|
|
95577
|
+
const retryCode = await runWorkerWithFixTask(ctx, "Reapply lost implementation files", [
|
|
95578
|
+
`The diff against \`${base2}\` contains only meta files`,
|
|
95579
|
+
`(openspec/tasks.md and similar). The substantive implementation`,
|
|
95580
|
+
`is missing from the branch \u2014 likely deleted by an earlier commit`,
|
|
95581
|
+
`or absorbed by a merge from origin/${base2}.`,
|
|
95582
|
+
"",
|
|
95583
|
+
`Files currently in the diff:`,
|
|
95584
|
+
fileList,
|
|
95585
|
+
"",
|
|
95586
|
+
`Re-apply the actual implementation work the change is supposed`,
|
|
95587
|
+
`to ship. Inspect git history (\`git log ${base2}..HEAD\`) to see`,
|
|
95588
|
+
`what was created earlier and lost, then restore those files`,
|
|
95589
|
+
`(or reproduce the work). Commit the restored files so the next`,
|
|
95590
|
+
`iteration's diff against \`${base2}\` contains real code, not`,
|
|
95591
|
+
`just meta files.`
|
|
95592
|
+
].join(`
|
|
95593
|
+
`));
|
|
95594
|
+
if (retryCode !== 0) {
|
|
95595
|
+
log2(`! worker re-run after only-meta block exited code ${retryCode} \u2014 giving up`, "red");
|
|
95596
|
+
return PR_FAILED_EXIT;
|
|
95597
|
+
}
|
|
95598
|
+
continue;
|
|
95599
|
+
}
|
|
95600
|
+
pr = attempt2.pr;
|
|
95601
|
+
break;
|
|
95602
|
+
}
|
|
95196
95603
|
if (!pr) {
|
|
95197
95604
|
log2(` no commits ahead of ${base2} \u2014 skipping PR`, "gray");
|
|
95198
95605
|
return 0;
|
|
95199
95606
|
}
|
|
95200
|
-
|
|
95201
|
-
|
|
95607
|
+
const prUrl = pr.url;
|
|
95608
|
+
if (!prUrl) {
|
|
95609
|
+
log2(`! PR creation returned a null URL for ${changeName} \u2014 giving up`, "red");
|
|
95610
|
+
return PR_FAILED_EXIT;
|
|
95611
|
+
}
|
|
95612
|
+
log2(` ${pr.created ? "opened" : "found existing"} PR: ${prUrl}`, "green");
|
|
95613
|
+
registerPr?.(changeName, prUrl);
|
|
95202
95614
|
let manualMergePending = false;
|
|
95203
95615
|
if (wantAutoMerge) {
|
|
95204
95616
|
const fallbackEnabled = cfg.manualMergeWhenAutoMergeDisabled !== false;
|
|
95205
|
-
const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(
|
|
95617
|
+
const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log2);
|
|
95206
95618
|
if (repoAllowsAutoMerge === false && fallbackEnabled) {
|
|
95207
|
-
log2(` repo has auto-merge disabled \u2014 will poll ${
|
|
95619
|
+
log2(` repo has auto-merge disabled \u2014 will poll ${prUrl} and merge via gh pr merge once checks pass`, "yellow");
|
|
95208
95620
|
manualMergePending = true;
|
|
95209
95621
|
} else {
|
|
95210
95622
|
try {
|
|
95211
|
-
await cmd.run(["gh", "pr", "merge",
|
|
95212
|
-
log2(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${
|
|
95623
|
+
await cmd.run(["gh", "pr", "merge", prUrl, "--auto", `--${cfg.autoMergeStrategy}`], cwd2);
|
|
95624
|
+
log2(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${prUrl}`, "green");
|
|
95213
95625
|
emit("auto-merge-enabled", cfg.autoMergeStrategy);
|
|
95214
95626
|
} catch (err) {
|
|
95215
95627
|
const e = err;
|
|
95216
95628
|
const detail = e.stderr?.trim() || e.message;
|
|
95217
|
-
log2(`! failed to enable auto-merge on ${
|
|
95629
|
+
log2(`! failed to enable auto-merge on ${prUrl}: ${detail}`, "yellow");
|
|
95218
95630
|
if (fallbackEnabled && /auto[- ]merge/i.test(detail)) {
|
|
95219
|
-
log2(` falling back to manual merge after CI passes for ${
|
|
95631
|
+
log2(` falling back to manual merge after CI passes for ${prUrl}`, "yellow");
|
|
95220
95632
|
manualMergePending = true;
|
|
95221
95633
|
}
|
|
95222
95634
|
}
|
|
95223
95635
|
}
|
|
95224
95636
|
}
|
|
95225
|
-
const ciResult = await fixConflictsAndCiLoop(ctx,
|
|
95637
|
+
const ciResult = await fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict);
|
|
95226
95638
|
if (ciResult !== 0)
|
|
95227
95639
|
return ciResult;
|
|
95228
95640
|
if (manualMergePending) {
|
|
95229
95641
|
try {
|
|
95230
|
-
await cmd.run(["gh", "pr", "merge",
|
|
95231
|
-
log2(` manually merged (${cfg.autoMergeStrategy}) ${
|
|
95642
|
+
await cmd.run(["gh", "pr", "merge", prUrl, `--${cfg.autoMergeStrategy}`], cwd2);
|
|
95643
|
+
log2(` manually merged (${cfg.autoMergeStrategy}) ${prUrl}`, "green");
|
|
95232
95644
|
emit("auto-merge-enabled", `manual:${cfg.autoMergeStrategy}`);
|
|
95233
95645
|
} catch (err) {
|
|
95234
95646
|
const e = err;
|
|
95235
|
-
log2(`! manual merge failed for ${
|
|
95647
|
+
log2(`! manual merge failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
|
|
95236
95648
|
}
|
|
95237
95649
|
}
|
|
95238
95650
|
return 0;
|
|
@@ -95333,6 +95745,8 @@ async function runPostTask(input, deps) {
|
|
|
95333
95745
|
var CI_FAILED_EXIT = 70, PR_FAILED_EXIT = 71, repoAutoMergeCache;
|
|
95334
95746
|
var init_post_task = __esm(() => {
|
|
95335
95747
|
init_tasks_md();
|
|
95748
|
+
init_linear();
|
|
95749
|
+
init_pr();
|
|
95336
95750
|
init_ci();
|
|
95337
95751
|
init_worktree();
|
|
95338
95752
|
repoAutoMergeCache = new Map;
|
|
@@ -95517,9 +95931,317 @@ var init_gate = __esm(() => {
|
|
|
95517
95931
|
FINGERPRINT_MARKER_RE = /<!--\s*ralphy:baseline:([a-f0-9]+)\s*-->/i;
|
|
95518
95932
|
});
|
|
95519
95933
|
|
|
95520
|
-
// apps/agent/src/agent/
|
|
95521
|
-
|
|
95934
|
+
// apps/agent/src/agent/linear-sync/index.ts
|
|
95935
|
+
function parseTasksMd(md) {
|
|
95936
|
+
const lines = md.split(/\r?\n/);
|
|
95937
|
+
const sections = [];
|
|
95938
|
+
let current = null;
|
|
95939
|
+
let i = 0;
|
|
95940
|
+
while (i < lines.length) {
|
|
95941
|
+
const line = lines[i];
|
|
95942
|
+
const headingMatch = /^##\s+(.+?)\s*$/.exec(line);
|
|
95943
|
+
if (headingMatch) {
|
|
95944
|
+
current = { heading: headingMatch[1], items: [] };
|
|
95945
|
+
sections.push(current);
|
|
95946
|
+
i += 1;
|
|
95947
|
+
continue;
|
|
95948
|
+
}
|
|
95949
|
+
const bulletMatch = /^(\s*)-\s+\[( |x|X)\]\s+(.+?)\s*$/.exec(line);
|
|
95950
|
+
if (bulletMatch && current) {
|
|
95951
|
+
const indent = bulletMatch[1] ?? "";
|
|
95952
|
+
const checked = bulletMatch[2]?.toLowerCase() === "x";
|
|
95953
|
+
const text = bulletMatch[3] ?? "";
|
|
95954
|
+
const bullet = `${indent}- [${checked ? "x" : " "}] ${text}`;
|
|
95955
|
+
i += 1;
|
|
95956
|
+
let j = i;
|
|
95957
|
+
while (j < lines.length && lines[j].trim() === "")
|
|
95958
|
+
j += 1;
|
|
95959
|
+
let code;
|
|
95960
|
+
if (j < lines.length && /^\s*```/.test(lines[j])) {
|
|
95961
|
+
const fenceOpen = lines[j];
|
|
95962
|
+
const fenceMatch = /^(\s*)```/.exec(fenceOpen);
|
|
95963
|
+
const fenceIndent = fenceMatch?.[1] ?? "";
|
|
95964
|
+
const buf = [];
|
|
95965
|
+
j += 1;
|
|
95966
|
+
while (j < lines.length) {
|
|
95967
|
+
if (new RegExp(`^${fenceIndent}\`\`\`\\s*$`).test(lines[j])) {
|
|
95968
|
+
j += 1;
|
|
95969
|
+
break;
|
|
95970
|
+
}
|
|
95971
|
+
buf.push(lines[j]);
|
|
95972
|
+
j += 1;
|
|
95973
|
+
}
|
|
95974
|
+
code = buf.join(`
|
|
95975
|
+
`);
|
|
95976
|
+
i = j;
|
|
95977
|
+
}
|
|
95978
|
+
current.items.push(code !== undefined ? { bullet, code } : { bullet });
|
|
95979
|
+
continue;
|
|
95980
|
+
}
|
|
95981
|
+
i += 1;
|
|
95982
|
+
}
|
|
95983
|
+
return sections;
|
|
95984
|
+
}
|
|
95985
|
+
function truncate4(s, max2) {
|
|
95986
|
+
if (s.length <= max2)
|
|
95987
|
+
return s;
|
|
95988
|
+
return `${s.slice(0, max2)}
|
|
95989
|
+
\u2026(truncated)`;
|
|
95990
|
+
}
|
|
95991
|
+
function renderTasksBlock(tasksMd, meta3) {
|
|
95992
|
+
const sections = parseTasksMd(tasksMd);
|
|
95993
|
+
const out = [];
|
|
95994
|
+
out.push(RALPHY_TASKS_START);
|
|
95995
|
+
out.push("### Ralph progress");
|
|
95996
|
+
out.push("");
|
|
95997
|
+
for (const section of sections) {
|
|
95998
|
+
if (section.items.length === 0)
|
|
95999
|
+
continue;
|
|
96000
|
+
out.push(`**${section.heading}**`);
|
|
96001
|
+
out.push("");
|
|
96002
|
+
for (const item of section.items) {
|
|
96003
|
+
out.push(item.bullet);
|
|
96004
|
+
if (item.code !== undefined) {
|
|
96005
|
+
const inner = truncate4(item.code, MAX_CODE_BLOCK_BYTES);
|
|
96006
|
+
out.push(` <details><summary>output</summary><pre>${inner}</pre></details>`);
|
|
96007
|
+
}
|
|
96008
|
+
}
|
|
96009
|
+
out.push("");
|
|
96010
|
+
}
|
|
96011
|
+
out.push(`<sub>\`${meta3.changeName}\` \xB7 iteration ${meta3.iteration}</sub>`);
|
|
96012
|
+
out.push(RALPHY_TASKS_END);
|
|
96013
|
+
return out.join(`
|
|
96014
|
+
`);
|
|
96015
|
+
}
|
|
96016
|
+
var RALPHY_TASKS_START = "<!-- ralphy:tasks:start -->", RALPHY_TASKS_END = "<!-- ralphy:tasks:end -->", MAX_CODE_BLOCK_BYTES;
|
|
96017
|
+
var init_linear_sync = __esm(() => {
|
|
96018
|
+
MAX_CODE_BLOCK_BYTES = 2 * 1024;
|
|
96019
|
+
});
|
|
96020
|
+
|
|
96021
|
+
// apps/agent/src/agent/linear-sync/comment-sync.ts
|
|
96022
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
95522
96023
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
96024
|
+
async function readStateJson(statePath) {
|
|
96025
|
+
const file2 = Bun.file(statePath);
|
|
96026
|
+
if (!await file2.exists())
|
|
96027
|
+
return null;
|
|
96028
|
+
try {
|
|
96029
|
+
return await file2.json();
|
|
96030
|
+
} catch {
|
|
96031
|
+
return null;
|
|
96032
|
+
}
|
|
96033
|
+
}
|
|
96034
|
+
async function writeStateJson(statePath, state) {
|
|
96035
|
+
await mkdir6(dirname7(statePath), { recursive: true });
|
|
96036
|
+
await Bun.write(statePath, JSON.stringify(state, null, 2) + `
|
|
96037
|
+
`);
|
|
96038
|
+
}
|
|
96039
|
+
function readComments(state) {
|
|
96040
|
+
const raw = state?.linearComments ?? {};
|
|
96041
|
+
return {
|
|
96042
|
+
planCommentId: raw?.planCommentId ?? null,
|
|
96043
|
+
tasksCommentId: raw?.tasksCommentId ?? null,
|
|
96044
|
+
planPostedAt: raw?.planPostedAt ?? null
|
|
96045
|
+
};
|
|
96046
|
+
}
|
|
96047
|
+
async function patchComments(statePath, patch) {
|
|
96048
|
+
const existing = await readStateJson(statePath) ?? {};
|
|
96049
|
+
const current = readComments(existing);
|
|
96050
|
+
const next = { ...current, ...patch };
|
|
96051
|
+
await writeStateJson(statePath, { ...existing, linearComments: next });
|
|
96052
|
+
}
|
|
96053
|
+
function isCommentNotFoundError(err) {
|
|
96054
|
+
if (!err)
|
|
96055
|
+
return false;
|
|
96056
|
+
const candidates = [];
|
|
96057
|
+
const e = err;
|
|
96058
|
+
if (Array.isArray(e.messages))
|
|
96059
|
+
candidates.push(...e.messages);
|
|
96060
|
+
if (typeof e.message === "string")
|
|
96061
|
+
candidates.push(e.message);
|
|
96062
|
+
const text = candidates.join(" ").toLowerCase();
|
|
96063
|
+
return text.includes("not found") || text.includes("could not find") || text.includes("entity not found");
|
|
96064
|
+
}
|
|
96065
|
+
async function readTasksMd(changeDir, log2) {
|
|
96066
|
+
const file2 = Bun.file(join21(changeDir, "tasks.md"));
|
|
96067
|
+
if (!await file2.exists()) {
|
|
96068
|
+
log2(` comment-sync: tasks.md missing in ${changeDir}, skipping`, "gray");
|
|
96069
|
+
return null;
|
|
96070
|
+
}
|
|
96071
|
+
try {
|
|
96072
|
+
return await file2.text();
|
|
96073
|
+
} catch (err) {
|
|
96074
|
+
log2(`! comment-sync: read tasks.md failed: ${err.message}`, "yellow");
|
|
96075
|
+
return null;
|
|
96076
|
+
}
|
|
96077
|
+
}
|
|
96078
|
+
function renderTasksCommentBody(tasksMd, changeName, iteration) {
|
|
96079
|
+
return renderTasksBlock(tasksMd, { changeName, iteration });
|
|
96080
|
+
}
|
|
96081
|
+
async function postOrUpdateTasksComment(deps) {
|
|
96082
|
+
const tasksMd = await readTasksMd(deps.changeDir, deps.log);
|
|
96083
|
+
if (!tasksMd)
|
|
96084
|
+
return null;
|
|
96085
|
+
const body = renderTasksCommentBody(tasksMd, deps.changeName, deps.iteration);
|
|
96086
|
+
const state = await readStateJson(deps.statePath);
|
|
96087
|
+
const comments = readComments(state);
|
|
96088
|
+
if (comments.tasksCommentId) {
|
|
96089
|
+
try {
|
|
96090
|
+
await deps.mutations.updateIssueComment(deps.apiKey, comments.tasksCommentId, body);
|
|
96091
|
+
deps.log(` comment-sync: updated tasks comment for ${deps.changeName}`, "gray");
|
|
96092
|
+
return comments.tasksCommentId;
|
|
96093
|
+
} catch (err) {
|
|
96094
|
+
if (!isCommentNotFoundError(err)) {
|
|
96095
|
+
deps.log(`! comment-sync: updateIssueComment failed: ${err.message}`, "yellow");
|
|
96096
|
+
return null;
|
|
96097
|
+
}
|
|
96098
|
+
deps.log(` comment-sync: tasks comment ${comments.tasksCommentId} not found \u2014 recreating`, "gray");
|
|
96099
|
+
}
|
|
96100
|
+
}
|
|
96101
|
+
let newId;
|
|
96102
|
+
try {
|
|
96103
|
+
newId = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
|
|
96104
|
+
} catch (err) {
|
|
96105
|
+
deps.log(`! comment-sync: createIssueComment failed: ${err.message}`, "yellow");
|
|
96106
|
+
return null;
|
|
96107
|
+
}
|
|
96108
|
+
await patchComments(deps.statePath, { tasksCommentId: newId });
|
|
96109
|
+
deps.log(` comment-sync: created tasks comment for ${deps.changeName}`, "gray");
|
|
96110
|
+
return newId;
|
|
96111
|
+
}
|
|
96112
|
+
function planningComplete(tasksMd) {
|
|
96113
|
+
const lines = tasksMd.split(/\r?\n/);
|
|
96114
|
+
let inPlanning = false;
|
|
96115
|
+
let total = 0;
|
|
96116
|
+
let unchecked = 0;
|
|
96117
|
+
for (const line of lines) {
|
|
96118
|
+
const h = /^##\s+(.+?)\s*$/.exec(line);
|
|
96119
|
+
if (h) {
|
|
96120
|
+
inPlanning = h[1].trim().toLowerCase() === "planning";
|
|
96121
|
+
continue;
|
|
96122
|
+
}
|
|
96123
|
+
if (!inPlanning)
|
|
96124
|
+
continue;
|
|
96125
|
+
const m = /^\s*-\s+\[( |x|X)\]/.exec(line);
|
|
96126
|
+
if (!m)
|
|
96127
|
+
continue;
|
|
96128
|
+
total += 1;
|
|
96129
|
+
if (m[1] === " ")
|
|
96130
|
+
unchecked += 1;
|
|
96131
|
+
}
|
|
96132
|
+
return { allChecked: total > 0 && unchecked === 0, total };
|
|
96133
|
+
}
|
|
96134
|
+
async function readFirstParagraph(path) {
|
|
96135
|
+
const file2 = Bun.file(path);
|
|
96136
|
+
if (!await file2.exists())
|
|
96137
|
+
return null;
|
|
96138
|
+
const text = await file2.text();
|
|
96139
|
+
const blocks = text.split(/\r?\n\s*\r?\n/).map((b) => b.trim()).filter((b) => b.length > 0 && !/^#\s/.test(b));
|
|
96140
|
+
return blocks[0] ?? null;
|
|
96141
|
+
}
|
|
96142
|
+
async function readSection(path, heading) {
|
|
96143
|
+
const file2 = Bun.file(path);
|
|
96144
|
+
if (!await file2.exists())
|
|
96145
|
+
return null;
|
|
96146
|
+
const text = await file2.text();
|
|
96147
|
+
const headingRe = new RegExp(`(^|\\n)##\\s+${heading}\\s*\\n`);
|
|
96148
|
+
const m = headingRe.exec(text);
|
|
96149
|
+
if (!m)
|
|
96150
|
+
return null;
|
|
96151
|
+
const start = m.index + m[0].length;
|
|
96152
|
+
const rest2 = text.slice(start);
|
|
96153
|
+
const next = /\n##\s+/.exec(rest2);
|
|
96154
|
+
const body = next ? rest2.slice(0, next.index) : rest2;
|
|
96155
|
+
return body.trim() || null;
|
|
96156
|
+
}
|
|
96157
|
+
async function postPlanCommentOnce(deps) {
|
|
96158
|
+
const state = await readStateJson(deps.statePath);
|
|
96159
|
+
const comments = readComments(state);
|
|
96160
|
+
if (comments.planCommentId)
|
|
96161
|
+
return null;
|
|
96162
|
+
const tasksMd = await readTasksMd(deps.changeDir, deps.log);
|
|
96163
|
+
if (!tasksMd)
|
|
96164
|
+
return null;
|
|
96165
|
+
const check2 = planningComplete(tasksMd);
|
|
96166
|
+
if (!check2.allChecked)
|
|
96167
|
+
return null;
|
|
96168
|
+
const proposalPath = join21(deps.changeDir, "proposal.md");
|
|
96169
|
+
const why = await readSection(proposalPath, "Why");
|
|
96170
|
+
const whatChanges = await readSection(proposalPath, "What Changes");
|
|
96171
|
+
if (!why && !whatChanges) {
|
|
96172
|
+
deps.log(` comment-sync: proposal.md has no Why/What Changes, skipping plan comment`, "gray");
|
|
96173
|
+
return null;
|
|
96174
|
+
}
|
|
96175
|
+
const designSummary = await readFirstParagraph(join21(deps.changeDir, "design.md"));
|
|
96176
|
+
const parts = [`### ${PLAN_COMMENT_TITLE} \u2014 \`${deps.changeName}\``];
|
|
96177
|
+
if (why) {
|
|
96178
|
+
parts.push("", "**Why**", "", why);
|
|
96179
|
+
}
|
|
96180
|
+
if (whatChanges) {
|
|
96181
|
+
parts.push("", "**What Changes**", "", whatChanges);
|
|
96182
|
+
}
|
|
96183
|
+
if (designSummary) {
|
|
96184
|
+
parts.push("", "**Design**", "", designSummary);
|
|
96185
|
+
}
|
|
96186
|
+
const body = parts.join(`
|
|
96187
|
+
`);
|
|
96188
|
+
let id;
|
|
96189
|
+
try {
|
|
96190
|
+
id = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
|
|
96191
|
+
} catch (err) {
|
|
96192
|
+
deps.log(`! comment-sync: plan comment create failed: ${err.message}`, "yellow");
|
|
96193
|
+
return null;
|
|
96194
|
+
}
|
|
96195
|
+
await patchComments(deps.statePath, {
|
|
96196
|
+
planCommentId: id,
|
|
96197
|
+
planPostedAt: new Date().toISOString()
|
|
96198
|
+
});
|
|
96199
|
+
deps.log(` comment-sync: posted plan comment for ${deps.changeName}`, "gray");
|
|
96200
|
+
return id;
|
|
96201
|
+
}
|
|
96202
|
+
async function postSteeringAndRefreshTasks(deps) {
|
|
96203
|
+
const firstLine = deps.message.split(/\r?\n/, 1)[0].trim() || deps.message.trim();
|
|
96204
|
+
const steeringBody = `### ${STEERING_COMMENT_TITLE}
|
|
96205
|
+
|
|
96206
|
+
${deps.message.trim()}`;
|
|
96207
|
+
try {
|
|
96208
|
+
await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, steeringBody);
|
|
96209
|
+
deps.log(` comment-sync: posted steering comment (${firstLine})`, "gray");
|
|
96210
|
+
} catch (err) {
|
|
96211
|
+
deps.log(`! comment-sync: steering comment create failed: ${err.message}`, "yellow");
|
|
96212
|
+
}
|
|
96213
|
+
const state = await readStateJson(deps.statePath);
|
|
96214
|
+
const comments = readComments(state);
|
|
96215
|
+
if (comments.tasksCommentId) {
|
|
96216
|
+
try {
|
|
96217
|
+
await deps.mutations.deleteIssueComment(deps.apiKey, comments.tasksCommentId);
|
|
96218
|
+
deps.log(` comment-sync: deleted old tasks comment`, "gray");
|
|
96219
|
+
} catch (err) {
|
|
96220
|
+
if (!isCommentNotFoundError(err)) {
|
|
96221
|
+
deps.log(`! comment-sync: deleteIssueComment failed: ${err.message}`, "yellow");
|
|
96222
|
+
}
|
|
96223
|
+
}
|
|
96224
|
+
await patchComments(deps.statePath, { tasksCommentId: null });
|
|
96225
|
+
}
|
|
96226
|
+
await postOrUpdateTasksComment({
|
|
96227
|
+
apiKey: deps.apiKey,
|
|
96228
|
+
issueId: deps.issueId,
|
|
96229
|
+
statePath: deps.statePath,
|
|
96230
|
+
changeDir: deps.changeDir,
|
|
96231
|
+
changeName: deps.changeName,
|
|
96232
|
+
log: deps.log,
|
|
96233
|
+
mutations: deps.mutations,
|
|
96234
|
+
iteration: deps.iteration
|
|
96235
|
+
});
|
|
96236
|
+
}
|
|
96237
|
+
var PLAN_COMMENT_TITLE = "\uD83D\uDCCB Ralph plan", STEERING_COMMENT_TITLE = "\uD83E\uDDED Ralph steering";
|
|
96238
|
+
var init_comment_sync = __esm(() => {
|
|
96239
|
+
init_linear_sync();
|
|
96240
|
+
});
|
|
96241
|
+
|
|
96242
|
+
// apps/agent/src/agent/wire.ts
|
|
96243
|
+
import { join as join22 } from "path";
|
|
96244
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
95523
96245
|
async function pickOpenPrUrlFromAttachments(urls, issueIdent, cmd, cwd2, onLog) {
|
|
95524
96246
|
const candidates = urls.filter((url2) => GITHUB_PR_URL_RE.test(url2));
|
|
95525
96247
|
let sawNonOpenPr = false;
|
|
@@ -95683,7 +96405,7 @@ function buildAgentCoordinator(input) {
|
|
|
95683
96405
|
onWorkerOutput,
|
|
95684
96406
|
onWorkerCmd
|
|
95685
96407
|
} = input;
|
|
95686
|
-
const logsDir =
|
|
96408
|
+
const logsDir = join22(projectRoot, ".ralph", "logs");
|
|
95687
96409
|
const concurrency = args.concurrency || cfg.concurrency;
|
|
95688
96410
|
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
95689
96411
|
const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
|
|
@@ -95806,6 +96528,7 @@ function buildAgentCoordinator(input) {
|
|
|
95806
96528
|
const prUnavailable = new Map;
|
|
95807
96529
|
const PR_UNAVAILABLE_TTL_MS = 10 * 60 * 1000;
|
|
95808
96530
|
const stalePingedAt = new Map;
|
|
96531
|
+
const lastHandledReviewActivity = new Map;
|
|
95809
96532
|
const useWorktree = args.worktree || cfg.useWorktree;
|
|
95810
96533
|
const scriptRunner = input.runners?.runScript ?? (async (cmd, cwd2) => {
|
|
95811
96534
|
const proc = Bun.spawn({
|
|
@@ -95895,8 +96618,8 @@ function buildAgentCoordinator(input) {
|
|
|
95895
96618
|
} else {
|
|
95896
96619
|
changeName = changeNameForIssue(issue2);
|
|
95897
96620
|
const wtLayout = projectLayout(workerCwd);
|
|
95898
|
-
await
|
|
95899
|
-
await
|
|
96621
|
+
await mkdir7(wtLayout.changeDir(changeName), { recursive: true });
|
|
96622
|
+
await mkdir7(wtLayout.taskStateDir(changeName), { recursive: true });
|
|
95900
96623
|
}
|
|
95901
96624
|
cwdByChange.set(changeName, workerCwd);
|
|
95902
96625
|
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
@@ -95905,7 +96628,7 @@ function buildAgentCoordinator(input) {
|
|
|
95905
96628
|
branchByChange.set(changeName, branch);
|
|
95906
96629
|
if (mode === "review") {
|
|
95907
96630
|
const wtLayout = projectLayout(workerCwd);
|
|
95908
|
-
const tasksFile =
|
|
96631
|
+
const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
|
|
95909
96632
|
let body;
|
|
95910
96633
|
let heading;
|
|
95911
96634
|
if (trigger) {
|
|
@@ -95930,7 +96653,7 @@ function buildAgentCoordinator(input) {
|
|
|
95930
96653
|
await reactivateState2(wtLayout.stateFile(changeName), changeName);
|
|
95931
96654
|
} else if (mode === "conflict-fix") {
|
|
95932
96655
|
const wtLayout = projectLayout(workerCwd);
|
|
95933
|
-
const tasksFile =
|
|
96656
|
+
const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
|
|
95934
96657
|
const prUrl = prByChange.get(changeName);
|
|
95935
96658
|
const body = [
|
|
95936
96659
|
`The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
|
|
@@ -96010,7 +96733,7 @@ PR: ${prUrl}` : ""
|
|
|
96010
96733
|
return c;
|
|
96011
96734
|
}
|
|
96012
96735
|
function defaultSpawn(changeName, cmd, cwd2, note) {
|
|
96013
|
-
const logFilePath =
|
|
96736
|
+
const logFilePath = join22(logsDir, `${changeName}.log`);
|
|
96014
96737
|
const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
|
|
96015
96738
|
const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
|
|
96016
96739
|
const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
|
|
@@ -96069,10 +96792,15 @@ PR: ${prUrl}` : ""
|
|
|
96069
96792
|
function spawnWorker(changeName) {
|
|
96070
96793
|
const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
|
|
96071
96794
|
const injected = input.runners?.spawnWorker;
|
|
96795
|
+
const missionTasksPath = join22(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
|
|
96796
|
+
const prevTasksPromise = (async () => {
|
|
96797
|
+
const f2 = Bun.file(missionTasksPath);
|
|
96798
|
+
return await f2.exists() ? await f2.text() : "";
|
|
96799
|
+
})();
|
|
96072
96800
|
let logFilePath;
|
|
96073
96801
|
let handle;
|
|
96074
96802
|
if (injected) {
|
|
96075
|
-
logFilePath =
|
|
96803
|
+
logFilePath = join22(logsDir, `${changeName}.log`);
|
|
96076
96804
|
handle = injected(buildTaskCmdFor(changeName), cwd2);
|
|
96077
96805
|
} else {
|
|
96078
96806
|
const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `spawn at ${new Date().toISOString()}`);
|
|
@@ -96094,6 +96822,21 @@ PR: ${prUrl}` : ""
|
|
|
96094
96822
|
const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
|
|
96095
96823
|
const wrapped = handle.exited.then(async (code) => {
|
|
96096
96824
|
const workerLayout = projectLayout(cwd2);
|
|
96825
|
+
try {
|
|
96826
|
+
const prevTasks = await prevTasksPromise;
|
|
96827
|
+
const nextFile = Bun.file(missionTasksPath);
|
|
96828
|
+
if (await nextFile.exists()) {
|
|
96829
|
+
const nextTasks = await nextFile.text();
|
|
96830
|
+
const report = normalizeNewlyAppendedSectionWithReport(prevTasks, nextTasks);
|
|
96831
|
+
if (report.text !== nextTasks) {
|
|
96832
|
+
await Bun.write(missionTasksPath, report.text);
|
|
96833
|
+
const sections = report.headings.map((h) => `## ${h}`).join(", ");
|
|
96834
|
+
onLog(`! normalized ${report.count} pre-checked item(s) in newly added section(s) ${sections}`, "yellow");
|
|
96835
|
+
}
|
|
96836
|
+
}
|
|
96837
|
+
} catch (err) {
|
|
96838
|
+
onLog(`! tasks.md normalization failed: ${err.message}`, "yellow");
|
|
96839
|
+
}
|
|
96097
96840
|
const effectiveCode = await runPostTask({
|
|
96098
96841
|
changeName,
|
|
96099
96842
|
cwd: cwd2,
|
|
@@ -96117,6 +96860,7 @@ PR: ${prUrl}` : ""
|
|
|
96117
96860
|
ignoreCiChecks: cfg.ignoreCiChecks,
|
|
96118
96861
|
stackPrsOnDependencies: args.stackPrs || cfg.stackPrsOnDependencies,
|
|
96119
96862
|
neverTouch: cfg.boundaries.never_touch,
|
|
96863
|
+
metaOnlyFiles: cfg.boundaries.meta_only_files,
|
|
96120
96864
|
manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled
|
|
96121
96865
|
},
|
|
96122
96866
|
respawnWorker: respawn
|
|
@@ -96327,19 +97071,34 @@ PR: ${prUrl}` : ""
|
|
|
96327
97071
|
const handle = cfg.linear.mentionHandle;
|
|
96328
97072
|
let candidates = [];
|
|
96329
97073
|
try {
|
|
96330
|
-
candidates = await
|
|
97074
|
+
candidates = await fetchMentionScanIssues(apiKey, { team, assignee });
|
|
96331
97075
|
} catch (err) {
|
|
96332
|
-
|
|
97076
|
+
if (isRateLimitedError(err)) {
|
|
97077
|
+
onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
|
|
97078
|
+
return [];
|
|
97079
|
+
}
|
|
97080
|
+
onLog(`! mention scan: fetchMentionScanIssues failed: ${formatLinearError(err)}`, "yellow");
|
|
96333
97081
|
return [];
|
|
96334
97082
|
}
|
|
96335
97083
|
const out = [];
|
|
96336
97084
|
const queued = new Set;
|
|
97085
|
+
let rateLimitedLogged = false;
|
|
97086
|
+
const logRateLimited = () => {
|
|
97087
|
+
if (rateLimitedLogged)
|
|
97088
|
+
return;
|
|
97089
|
+
rateLimitedLogged = true;
|
|
97090
|
+
onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
|
|
97091
|
+
};
|
|
96337
97092
|
for (const issue2 of candidates) {
|
|
96338
97093
|
let comments = [];
|
|
96339
97094
|
try {
|
|
96340
97095
|
comments = await fetchIssueComments(apiKey, issue2.id);
|
|
96341
97096
|
} catch (err) {
|
|
96342
|
-
|
|
97097
|
+
if (isRateLimitedError(err)) {
|
|
97098
|
+
logRateLimited();
|
|
97099
|
+
break;
|
|
97100
|
+
}
|
|
97101
|
+
onLog(`! mention scan: Linear comments failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
|
|
96343
97102
|
continue;
|
|
96344
97103
|
}
|
|
96345
97104
|
const lastRalphPickup = findLastRalphPickupISO(comments);
|
|
@@ -96364,11 +97123,18 @@ PR: ${prUrl}` : ""
|
|
|
96364
97123
|
try {
|
|
96365
97124
|
await addReactionToComment(apiKey, c.id, "\uD83D\uDC40");
|
|
96366
97125
|
} catch (err) {
|
|
96367
|
-
|
|
97126
|
+
if (isRateLimitedError(err)) {
|
|
97127
|
+
logRateLimited();
|
|
97128
|
+
queued.add(issue2.id);
|
|
97129
|
+
break;
|
|
97130
|
+
}
|
|
97131
|
+
onLog(`! mention scan: Linear reaction failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
|
|
96368
97132
|
}
|
|
96369
97133
|
queued.add(issue2.id);
|
|
96370
97134
|
break;
|
|
96371
97135
|
}
|
|
97136
|
+
if (rateLimitedLogged)
|
|
97137
|
+
break;
|
|
96372
97138
|
if (queued.has(issue2.id))
|
|
96373
97139
|
continue;
|
|
96374
97140
|
}
|
|
@@ -96398,7 +97164,7 @@ PR: ${prUrl}` : ""
|
|
|
96398
97164
|
try {
|
|
96399
97165
|
await addGithubReactionToComment({ owner, repo, kind: "issue" }, c.id, "\uD83D\uDC40");
|
|
96400
97166
|
} catch (err) {
|
|
96401
|
-
onLog(`! mention scan: GitHub reaction failed for ${prUrl}: ${err
|
|
97167
|
+
onLog(`! mention scan: GitHub reaction failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
|
|
96402
97168
|
}
|
|
96403
97169
|
}
|
|
96404
97170
|
queued.add(issue2.id);
|
|
@@ -96428,7 +97194,9 @@ PR: ${prUrl}` : ""
|
|
|
96428
97194
|
const last2 = t.comments[t.comments.length - 1].createdAt;
|
|
96429
97195
|
return last2 > acc ? last2 : acc;
|
|
96430
97196
|
}, "");
|
|
96431
|
-
|
|
97197
|
+
const lastHandled = lastHandledReviewActivity.get(prUrl) ?? null;
|
|
97198
|
+
const effectiveLastHandled = lastRalphPickup && lastHandled ? lastRalphPickup > lastHandled ? lastRalphPickup : lastHandled : lastRalphPickup ?? lastHandled;
|
|
97199
|
+
if (!effectiveLastHandled || newestReviewerActivity > effectiveLastHandled) {
|
|
96432
97200
|
const body = unresolved.map((t) => {
|
|
96433
97201
|
const head3 = t.path ? `_${t.path}${t.line ? `:${t.line}` : ""}_` : "_(general)_";
|
|
96434
97202
|
const lines = t.comments.map((c) => `> **${c.author ?? "reviewer"}** (${c.createdAt})
|
|
@@ -96442,6 +97210,7 @@ PR: ${prUrl}` : ""
|
|
|
96442
97210
|
---
|
|
96443
97211
|
|
|
96444
97212
|
`);
|
|
97213
|
+
lastHandledReviewActivity.set(prUrl, newestReviewerActivity);
|
|
96445
97214
|
return {
|
|
96446
97215
|
source: "github-review",
|
|
96447
97216
|
body,
|
|
@@ -96589,10 +97358,16 @@ PR: ${prUrl}` : ""
|
|
|
96589
97358
|
const parsed = JSON.parse(res.stdout || "[]");
|
|
96590
97359
|
return parsed;
|
|
96591
97360
|
} catch (err) {
|
|
96592
|
-
onLog(`! mention scan: gh comments failed for ${prUrl}: ${err
|
|
97361
|
+
onLog(`! mention scan: gh comments failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
|
|
96593
97362
|
return [];
|
|
96594
97363
|
}
|
|
96595
97364
|
}
|
|
97365
|
+
const commentSyncEnabled = Boolean(cfg.linear.syncTasksToComment && apiKey);
|
|
97366
|
+
const commentMutations = {
|
|
97367
|
+
createIssueComment,
|
|
97368
|
+
updateIssueComment,
|
|
97369
|
+
deleteIssueComment
|
|
97370
|
+
};
|
|
96596
97371
|
const coord = new AgentCoordinator({
|
|
96597
97372
|
fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
|
|
96598
97373
|
fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
|
|
@@ -96620,7 +97395,65 @@ PR: ${prUrl}` : ""
|
|
|
96620
97395
|
return 0;
|
|
96621
97396
|
const json2 = await file2.json();
|
|
96622
97397
|
return json2.iteration ?? 0;
|
|
96623
|
-
}
|
|
97398
|
+
},
|
|
97399
|
+
...commentSyncEnabled ? {
|
|
97400
|
+
syncTasks: async (worker, iteration) => {
|
|
97401
|
+
const root = cwdByChange.get(worker.changeName) ?? projectRoot;
|
|
97402
|
+
const layout = projectLayout(root);
|
|
97403
|
+
const changeDir = layout.changeDir(worker.changeName);
|
|
97404
|
+
const statePath = layout.stateFile(worker.changeName);
|
|
97405
|
+
await postPlanCommentOnce({
|
|
97406
|
+
apiKey,
|
|
97407
|
+
issueId: worker.issueId,
|
|
97408
|
+
statePath,
|
|
97409
|
+
changeDir,
|
|
97410
|
+
changeName: worker.changeName,
|
|
97411
|
+
log: onLog,
|
|
97412
|
+
mutations: commentMutations
|
|
97413
|
+
});
|
|
97414
|
+
await postOrUpdateTasksComment({
|
|
97415
|
+
apiKey,
|
|
97416
|
+
issueId: worker.issueId,
|
|
97417
|
+
statePath,
|
|
97418
|
+
changeDir,
|
|
97419
|
+
changeName: worker.changeName,
|
|
97420
|
+
iteration,
|
|
97421
|
+
log: onLog,
|
|
97422
|
+
mutations: commentMutations
|
|
97423
|
+
});
|
|
97424
|
+
},
|
|
97425
|
+
onSteeringAppended: async (changeName, message) => {
|
|
97426
|
+
const root = cwdByChange.get(changeName) ?? projectRoot;
|
|
97427
|
+
const layout = projectLayout(root);
|
|
97428
|
+
const changeDir = layout.changeDir(changeName);
|
|
97429
|
+
const statePath = layout.stateFile(changeName);
|
|
97430
|
+
const issue2 = issueByChange.get(changeName) ?? null;
|
|
97431
|
+
const issueId = issue2?.id ?? null;
|
|
97432
|
+
if (!issueId) {
|
|
97433
|
+
onLog(` comment-sync: no Linear issue cached for ${changeName}; skipping steering refresh`, "gray");
|
|
97434
|
+
return;
|
|
97435
|
+
}
|
|
97436
|
+
let iteration = 0;
|
|
97437
|
+
try {
|
|
97438
|
+
const f2 = Bun.file(statePath);
|
|
97439
|
+
if (await f2.exists()) {
|
|
97440
|
+
const json2 = await f2.json();
|
|
97441
|
+
iteration = json2.iteration ?? 0;
|
|
97442
|
+
}
|
|
97443
|
+
} catch {}
|
|
97444
|
+
await postSteeringAndRefreshTasks({
|
|
97445
|
+
apiKey,
|
|
97446
|
+
issueId,
|
|
97447
|
+
statePath,
|
|
97448
|
+
changeDir,
|
|
97449
|
+
changeName,
|
|
97450
|
+
iteration,
|
|
97451
|
+
message,
|
|
97452
|
+
log: onLog,
|
|
97453
|
+
mutations: commentMutations
|
|
97454
|
+
});
|
|
97455
|
+
}
|
|
97456
|
+
} : {}
|
|
96624
97457
|
}, {
|
|
96625
97458
|
concurrency,
|
|
96626
97459
|
...indicators.setInProgress !== undefined ? { setInProgress: indicators.setInProgress } : {},
|
|
@@ -96687,6 +97520,7 @@ PR: ${prUrl}` : ""
|
|
|
96687
97520
|
concurrency,
|
|
96688
97521
|
pollInterval,
|
|
96689
97522
|
getWorkerCwd: (changeName) => cwdByChange.get(changeName),
|
|
97523
|
+
syncTasksEnabled: commentSyncEnabled,
|
|
96690
97524
|
runBaselineGate: runBaselineGateOnce
|
|
96691
97525
|
};
|
|
96692
97526
|
}
|
|
@@ -96715,6 +97549,7 @@ var init_wire = __esm(() => {
|
|
|
96715
97549
|
init_tasks_md();
|
|
96716
97550
|
init_workflow();
|
|
96717
97551
|
init_types2();
|
|
97552
|
+
init_linear();
|
|
96718
97553
|
init_coordinator();
|
|
96719
97554
|
init_scaffold();
|
|
96720
97555
|
init_worktree();
|
|
@@ -96722,6 +97557,7 @@ var init_wire = __esm(() => {
|
|
|
96722
97557
|
init_post_task();
|
|
96723
97558
|
init_gate();
|
|
96724
97559
|
init_workflow();
|
|
97560
|
+
init_comment_sync();
|
|
96725
97561
|
GITHUB_PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
|
|
96726
97562
|
bunGitRunner = {
|
|
96727
97563
|
run: async (args, cwd2) => {
|
|
@@ -96840,21 +97676,26 @@ function readSize2() {
|
|
|
96840
97676
|
rows: process.stdout.rows ?? 24
|
|
96841
97677
|
};
|
|
96842
97678
|
}
|
|
97679
|
+
function clearScreenAndScrollback2() {
|
|
97680
|
+
if (process.stdout.isTTY)
|
|
97681
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
97682
|
+
}
|
|
96843
97683
|
function useTerminalSize2() {
|
|
96844
|
-
const
|
|
96845
|
-
|
|
96846
|
-
|
|
96847
|
-
}));
|
|
97684
|
+
const initial2 = import_react59.useRef({ ...readSize2(), resizeKey: 0 });
|
|
97685
|
+
const [size2, setSize] = import_react59.useState(initial2.current);
|
|
97686
|
+
const sizeRef = import_react59.useRef(initial2.current);
|
|
96848
97687
|
import_react59.useEffect(() => {
|
|
96849
97688
|
if (!process.stdout.isTTY)
|
|
96850
97689
|
return;
|
|
96851
97690
|
const onResize = () => {
|
|
96852
97691
|
const { columns, rows } = readSize2();
|
|
96853
|
-
|
|
96854
|
-
|
|
96855
|
-
|
|
96856
|
-
|
|
96857
|
-
}
|
|
97692
|
+
const prev = sizeRef.current;
|
|
97693
|
+
if (prev.columns === columns && prev.rows === rows)
|
|
97694
|
+
return;
|
|
97695
|
+
clearScreenAndScrollback2();
|
|
97696
|
+
const next = { columns, rows, resizeKey: prev.resizeKey + 1 };
|
|
97697
|
+
sizeRef.current = next;
|
|
97698
|
+
setSize(next);
|
|
96858
97699
|
};
|
|
96859
97700
|
process.stdout.on("resize", onResize);
|
|
96860
97701
|
return () => {
|
|
@@ -97044,7 +97885,7 @@ var init_SteeringField = __esm(async () => {
|
|
|
97044
97885
|
});
|
|
97045
97886
|
|
|
97046
97887
|
// apps/agent/src/components/AgentMode.tsx
|
|
97047
|
-
import { join as
|
|
97888
|
+
import { join as join23 } from "path";
|
|
97048
97889
|
async function appendSteeringImpl(changeDir, message) {
|
|
97049
97890
|
await runWithContext(createDefaultContext(), async () => {
|
|
97050
97891
|
appendSteeringMessage(changeDir, message);
|
|
@@ -97295,24 +98136,19 @@ function AgentMode({
|
|
|
97295
98136
|
loadConfig = loadRalphyConfig
|
|
97296
98137
|
}) {
|
|
97297
98138
|
const { exit } = use_app_default();
|
|
97298
|
-
const { stdout } = use_stdout_default();
|
|
97299
98139
|
const { isRawModeSupported } = use_stdin_default();
|
|
97300
98140
|
const { columns, rows, resizeKey } = useTerminalSize2();
|
|
97301
|
-
import_react61.useEffect(() => {
|
|
97302
|
-
if (resizeKey === 0)
|
|
97303
|
-
return;
|
|
97304
|
-
stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
97305
|
-
}, [resizeKey, stdout]);
|
|
97306
98141
|
const [logs, setLogs] = import_react61.useState([]);
|
|
97307
98142
|
const [, setTick] = import_react61.useState(0);
|
|
97308
98143
|
const [clock, setClock] = import_react61.useState(0);
|
|
97309
98144
|
const [focusedIdx, setFocusedIdx] = import_react61.useState(0);
|
|
97310
|
-
const [showPendingTasks, setShowPendingTasks] = import_react61.useState(
|
|
98145
|
+
const [showPendingTasks, setShowPendingTasks] = import_react61.useState(false);
|
|
97311
98146
|
const [showAllSubtasks, setShowAllSubtasks] = import_react61.useState(false);
|
|
97312
98147
|
const coordRef = import_react61.useRef(null);
|
|
97313
98148
|
const workerMetaRef = import_react61.useRef(new Map);
|
|
97314
98149
|
const nextPollAtRef = import_react61.useRef(0);
|
|
97315
98150
|
const cfgRef = import_react61.useRef(null);
|
|
98151
|
+
const [effective, setEffective] = import_react61.useState(null);
|
|
97316
98152
|
const [pollStatus, setPollStatus] = import_react61.useState({
|
|
97317
98153
|
state: "idle",
|
|
97318
98154
|
lastFound: null,
|
|
@@ -97411,6 +98247,7 @@ function AgentMode({
|
|
|
97411
98247
|
m.prUrl = prUrl;
|
|
97412
98248
|
}
|
|
97413
98249
|
});
|
|
98250
|
+
setEffective({ concurrency, pollInterval });
|
|
97414
98251
|
coordRef.current = coord2;
|
|
97415
98252
|
await coord2.init();
|
|
97416
98253
|
const tick = async () => {
|
|
@@ -97503,7 +98340,7 @@ function AgentMode({
|
|
|
97503
98340
|
(async () => {
|
|
97504
98341
|
for (const [changeName, meta3] of workerMetaRef.current) {
|
|
97505
98342
|
try {
|
|
97506
|
-
const file2 = Bun.file(
|
|
98343
|
+
const file2 = Bun.file(join23(meta3.statesDir, changeName, ".ralph-state.json"));
|
|
97507
98344
|
if (await file2.exists()) {
|
|
97508
98345
|
const json2 = await file2.json();
|
|
97509
98346
|
meta3.iter = json2.iteration ?? meta3.iter;
|
|
@@ -97513,9 +98350,9 @@ function AgentMode({
|
|
|
97513
98350
|
}
|
|
97514
98351
|
if (meta3.changeDir) {
|
|
97515
98352
|
try {
|
|
97516
|
-
const tasksFile = Bun.file(
|
|
97517
|
-
const proposalFile = Bun.file(
|
|
97518
|
-
const designFile = Bun.file(
|
|
98353
|
+
const tasksFile = Bun.file(join23(meta3.changeDir, "tasks.md"));
|
|
98354
|
+
const proposalFile = Bun.file(join23(meta3.changeDir, "proposal.md"));
|
|
98355
|
+
const designFile = Bun.file(join23(meta3.changeDir, "design.md"));
|
|
97519
98356
|
const [tasksText, proposalText, designText] = await Promise.all([
|
|
97520
98357
|
tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
|
|
97521
98358
|
proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
|
|
@@ -97670,14 +98507,14 @@ function AgentMode({
|
|
|
97670
98507
|
dimColor: true,
|
|
97671
98508
|
children: [
|
|
97672
98509
|
" \u2502 \xD7",
|
|
97673
|
-
cfg.concurrency
|
|
98510
|
+
effective?.concurrency ?? cfg.concurrency
|
|
97674
98511
|
]
|
|
97675
98512
|
}, undefined, true, undefined, this),
|
|
97676
98513
|
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
97677
98514
|
dimColor: true,
|
|
97678
98515
|
children: [
|
|
97679
98516
|
" \u2502 poll ",
|
|
97680
|
-
cfg.pollIntervalSeconds,
|
|
98517
|
+
effective?.pollInterval ?? cfg.pollIntervalSeconds,
|
|
97681
98518
|
"s"
|
|
97682
98519
|
]
|
|
97683
98520
|
}, undefined, true, undefined, this),
|
|
@@ -97794,18 +98631,6 @@ function AgentMode({
|
|
|
97794
98631
|
dimColor: true,
|
|
97795
98632
|
children: "\xB7"
|
|
97796
98633
|
}, undefined, false, undefined, this),
|
|
97797
|
-
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
97798
|
-
dimColor: true,
|
|
97799
|
-
children: "conflict"
|
|
97800
|
-
}, undefined, false, undefined, this),
|
|
97801
|
-
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
97802
|
-
color: pollStatus.lastBuckets.conflicted > 0 ? "red" : "white",
|
|
97803
|
-
children: pollStatus.lastBuckets.conflicted
|
|
97804
|
-
}, undefined, false, undefined, this),
|
|
97805
|
-
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
97806
|
-
dimColor: true,
|
|
97807
|
-
children: "\xB7"
|
|
97808
|
-
}, undefined, false, undefined, this),
|
|
97809
98634
|
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
97810
98635
|
dimColor: true,
|
|
97811
98636
|
children: "review"
|
|
@@ -97836,6 +98661,7 @@ function AgentMode({
|
|
|
97836
98661
|
children: [
|
|
97837
98662
|
secsToNextPoll !== null ? /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
|
|
97838
98663
|
gap: 1,
|
|
98664
|
+
width: 7,
|
|
97839
98665
|
children: [
|
|
97840
98666
|
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
97841
98667
|
dimColor: true,
|
|
@@ -98357,11 +99183,14 @@ function AgentMode({
|
|
|
98357
99183
|
},
|
|
98358
99184
|
onSubmit: async (message) => {
|
|
98359
99185
|
try {
|
|
98360
|
-
await appendSteering(
|
|
99186
|
+
await appendSteering(join23(tasksDir, w.changeName), message);
|
|
98361
99187
|
} catch (err) {
|
|
98362
99188
|
appendLog(`! steering append failed for ${w.changeName}: ${err.message}`, "red");
|
|
98363
99189
|
throw err;
|
|
98364
99190
|
}
|
|
99191
|
+
try {
|
|
99192
|
+
await coordRef.current?.notifySteeringAppended?.(w.changeName, message);
|
|
99193
|
+
} catch {}
|
|
98365
99194
|
const restarted = await coordRef.current?.restartWorker(w.changeName);
|
|
98366
99195
|
if (restarted) {
|
|
98367
99196
|
appendLog(` ${w.changeName}: steering applied, restarting worker`, "cyan");
|
|
@@ -98561,7 +99390,7 @@ var exports_list = {};
|
|
|
98561
99390
|
__export(exports_list, {
|
|
98562
99391
|
runList: () => runList
|
|
98563
99392
|
});
|
|
98564
|
-
import { join as
|
|
99393
|
+
import { join as join24 } from "path";
|
|
98565
99394
|
function countTaskItems(content) {
|
|
98566
99395
|
const checked = (content.match(/^- \[x\]/gm) ?? []).length;
|
|
98567
99396
|
const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
|
|
@@ -98574,13 +99403,13 @@ function buildLocalRows(statesDir, projectRoot) {
|
|
|
98574
99403
|
const sources = [{ dir: statesDir, label: "main" }];
|
|
98575
99404
|
const worktreesRoot = worktreesDir2(projectRoot);
|
|
98576
99405
|
for (const wt of storage.list(worktreesRoot)) {
|
|
98577
|
-
sources.push({ dir:
|
|
99406
|
+
sources.push({ dir: join24(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
|
|
98578
99407
|
}
|
|
98579
99408
|
for (const { dir, label } of sources) {
|
|
98580
99409
|
for (const entry of storage.list(dir)) {
|
|
98581
99410
|
if (seen.has(entry))
|
|
98582
99411
|
continue;
|
|
98583
|
-
const raw = storage.read(
|
|
99412
|
+
const raw = storage.read(join24(dir, entry, ".ralph-state.json"));
|
|
98584
99413
|
if (raw === null)
|
|
98585
99414
|
continue;
|
|
98586
99415
|
let state;
|
|
@@ -98595,7 +99424,7 @@ function buildLocalRows(statesDir, projectRoot) {
|
|
|
98595
99424
|
const firstLine = promptRaw.split(`
|
|
98596
99425
|
`).find((l) => l.trim() !== "") ?? "";
|
|
98597
99426
|
let progress = "\u2014";
|
|
98598
|
-
const tasksContent = storage.read(
|
|
99427
|
+
const tasksContent = storage.read(join24(dir, entry, "tasks.md"));
|
|
98599
99428
|
if (tasksContent !== null) {
|
|
98600
99429
|
const { checked, unchecked } = countTaskItems(tasksContent);
|
|
98601
99430
|
const total = checked + unchecked;
|
|
@@ -98989,6 +99818,7 @@ var init_list = __esm(() => {
|
|
|
98989
99818
|
init_types2();
|
|
98990
99819
|
init_worktree();
|
|
98991
99820
|
init_config();
|
|
99821
|
+
init_linear();
|
|
98992
99822
|
init_list_sort();
|
|
98993
99823
|
localCmdRunner = {
|
|
98994
99824
|
run: async (cmd, cwd2) => {
|
|
@@ -99011,8 +99841,8 @@ var exports_json_runner = {};
|
|
|
99011
99841
|
__export(exports_json_runner, {
|
|
99012
99842
|
runAgentJson: () => runAgentJson
|
|
99013
99843
|
});
|
|
99014
|
-
import { join as
|
|
99015
|
-
import { mkdir as
|
|
99844
|
+
import { join as join25 } from "path";
|
|
99845
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
99016
99846
|
import { homedir as homedir5 } from "os";
|
|
99017
99847
|
function cleanOutputLine2(raw) {
|
|
99018
99848
|
const clean = raw.replace(ANSI_STRIP_RE2, "").trim();
|
|
@@ -99036,7 +99866,7 @@ async function runAgentJson({
|
|
|
99036
99866
|
statesDir,
|
|
99037
99867
|
tasksDir
|
|
99038
99868
|
}) {
|
|
99039
|
-
await
|
|
99869
|
+
await mkdir8(join25(homedir5(), ".ralph"), { recursive: true }).catch(() => {
|
|
99040
99870
|
return;
|
|
99041
99871
|
});
|
|
99042
99872
|
const cfgPath = await ensureRalphyConfig(projectRoot);
|
|
@@ -99190,8 +100020,8 @@ var exports_src2 = {};
|
|
|
99190
100020
|
__export(exports_src2, {
|
|
99191
100021
|
main: () => main2
|
|
99192
100022
|
});
|
|
99193
|
-
import { mkdir as
|
|
99194
|
-
import { join as
|
|
100023
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
100024
|
+
import { join as join26 } from "path";
|
|
99195
100025
|
async function main2(argv) {
|
|
99196
100026
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
99197
100027
|
printHelp2();
|
|
@@ -99225,9 +100055,9 @@ async function main2(argv) {
|
|
|
99225
100055
|
});
|
|
99226
100056
|
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
99227
100057
|
}
|
|
99228
|
-
await
|
|
99229
|
-
await
|
|
99230
|
-
await
|
|
100058
|
+
await mkdir9(statesDir, { recursive: true });
|
|
100059
|
+
await mkdir9(tasksDir, { recursive: true });
|
|
100060
|
+
await mkdir9(join26(projectRoot, ".ralph"), { recursive: true });
|
|
99231
100061
|
if (args.jsonOutput) {
|
|
99232
100062
|
const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
|
|
99233
100063
|
await runAgentJson2({ args, projectRoot, statesDir, tasksDir });
|