@neriros/ralphy 3.8.10 β 3.8.13
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 +45 -1
- package/dist/mcp/index.js +2 -2
- package/dist/shell/index.js +369 -114
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,13 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@neriros/ralphy)
|
|
4
4
|
[](https://www.npmjs.com/package/@neriros/ralphy)
|
|
5
|
-
[](https://github.com/
|
|
5
|
+
[](https://github.com/rosneri/ralphy/blob/main/LICENSE)
|
|
6
6
|
[](https://bun.sh)
|
|
7
7
|
|
|
8
8
|
An iterative AI task execution framework. Ralphy runs Claude or Codex in a checklist-driven loop with state on disk, cost safeguards, and a long-lived **agent** that polls Linear, opens PRs, and iterates with reviewers.
|
|
9
9
|
|
|
10
10
|
> π Full reference β Linear indicators, lifecycle, PR/CI flow, CLI flags, MCP β lives in **[GUIDE.md](./GUIDE.md)**.
|
|
11
11
|
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
**Loop**
|
|
15
|
+
|
|
16
|
+
- **Checklist-driven** β one unchecked task per iteration; state persists on disk so any run can be resumed.
|
|
17
|
+
- **Engine choice** β Claude (haiku / sonnet / opus) or Codex, swappable per task.
|
|
18
|
+
- **Safeguards** β `--max-iterations`, `--max-cost`, `--max-runtime`, `--max-failures` cap any runaway run.
|
|
19
|
+
- **OpenSpec layout** β `proposal.md` (steering) + `design.md` + `tasks.md` + `specs/` per change.
|
|
20
|
+
|
|
21
|
+
**Agent mode (Linear-driven)**
|
|
22
|
+
|
|
23
|
+
- **Linear polling** β picks up Todo tickets, resumes In Progress, re-runs reviewer-flagged Done.
|
|
24
|
+
- **Indicators** β declarative `WORKFLOW.md` map for "which labels/statuses to watch and apply" at each lifecycle event.
|
|
25
|
+
- **Worktrees** β every task runs in its own `git worktree` so concurrent workers can't stomp on each other.
|
|
26
|
+
- **Confirmation gate** β optional human approval step between `tasks` and `implement`; revise via `@ralphy revise: <why>`.
|
|
27
|
+
- **Self-review phase** β once tasks are checked off, an in-process reviewer can append more work for another round.
|
|
28
|
+
- **Tmux session management** β `ralphy agent` re-execs into a managed tmux session so detaching the terminal doesn't kill the loop.
|
|
29
|
+
- **Pre-existing error check** β pauses pickups when the trunk is red so the agent doesn't chase failures it didn't cause.
|
|
30
|
+
|
|
31
|
+
**PR + CI**
|
|
32
|
+
|
|
33
|
+
- **Auto PR open** β push branch and `gh pr create` on clean exit; idempotent (surfaces existing PR if open).
|
|
34
|
+
- **Auto-merge opt-in** β `getAutoMerge` triggers `gh pr merge --auto --squash|merge|rebase` right after PR creation.
|
|
35
|
+
- **Stacked PRs** β `--stack-prs` opens against a blocker's head branch when a `blocked_by` Linear relation has exactly one open PR.
|
|
36
|
+
- **CI fix loop** β on red CI, pulls failed logs, appends to steering, re-spawns until green or `maxCiFixAttempts` hit.
|
|
37
|
+
- **Conflict re-fix** β `gh pr view`βdriven; on `mergeable: CONFLICTING` enqueues a conflict-resolution task automatically.
|
|
38
|
+
|
|
39
|
+
**Reviewer interaction**
|
|
40
|
+
|
|
41
|
+
- **`@ralphy` mentions** β Linear comments _and_ GitHub PR comments trigger a fresh review run with the mention as the prompt.
|
|
42
|
+
- **Code-review iteration** β unresolved review-thread comments queue a digest; Ralph agrees-and-fixes (resolving the thread) or disagrees-and-replies.
|
|
43
|
+
- **Sticky task comment** β `tasks.md` mirrors into a single Linear comment that updates in place; a one-shot "π Plan" comment summarises proposal + design when planning completes.
|
|
44
|
+
|
|
45
|
+
**Observability**
|
|
46
|
+
|
|
47
|
+
- **Ink dashboard** β engine/model, poll-bucket breakdown, per-worker cards with live phase, command-in-flight, and stdout tail.
|
|
48
|
+
- **Structured JSON event stream** β `--json-output` for CI; `--json-log-file` mirrors the same stream to disk.
|
|
49
|
+
- **Per-worker logs** β `~/.ralph/agent-mode.log` (global) + `.ralph/logs/<change>.log` (per-task) + per-change `LOG.jsonl`.
|
|
50
|
+
|
|
51
|
+
**Extensibility**
|
|
52
|
+
|
|
53
|
+
- **MCP server** β exposes `ralph_list_changes` / `get_change` / `create_change` / `append_steering` / `stop` to Claude-side agents (auto-wired on per-project install).
|
|
54
|
+
- **`WORKFLOW.md` template body** β Jinja-style prompt rendered per iteration, so project-specific rules / boundaries / labels flow into every task automatically.
|
|
55
|
+
|
|
12
56
|
## How it works
|
|
13
57
|
|
|
14
58
|
```mermaid
|
package/dist/mcp/index.js
CHANGED
|
@@ -24066,6 +24066,7 @@ var StateSchema = exports_external.object({
|
|
|
24066
24066
|
model: exports_external.string().default("opus"),
|
|
24067
24067
|
manualTest: exports_external.boolean().default(false),
|
|
24068
24068
|
createPr: exports_external.boolean().default(false),
|
|
24069
|
+
prDraft: exports_external.boolean().default(false),
|
|
24069
24070
|
validateOnComplete: exports_external.boolean().default(false),
|
|
24070
24071
|
usage: UsageSchema.default({}),
|
|
24071
24072
|
history: exports_external.array(HistoryEntrySchema).default([]),
|
|
@@ -24183,6 +24184,7 @@ function buildInitialState(options) {
|
|
|
24183
24184
|
model: options.model ?? "opus",
|
|
24184
24185
|
manualTest: options.manualTest ?? false,
|
|
24185
24186
|
createPr: options.createPr ?? false,
|
|
24187
|
+
prDraft: options.prDraft ?? false,
|
|
24186
24188
|
createdAt: now,
|
|
24187
24189
|
lastModified: now,
|
|
24188
24190
|
metadata: { branch }
|
|
@@ -25120,8 +25122,6 @@ class OpenSpecChangeStore {
|
|
|
25120
25122
|
} catch {}
|
|
25121
25123
|
}
|
|
25122
25124
|
const changesDir = join4("openspec", "changes");
|
|
25123
|
-
if (!await Bun.file(changesDir).exists())
|
|
25124
|
-
return [];
|
|
25125
25125
|
try {
|
|
25126
25126
|
const entries = await readdir(changesDir, { withFileTypes: true });
|
|
25127
25127
|
return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").map((entry) => entry.name);
|
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.8.
|
|
18932
|
-
return "3.8.
|
|
18931
|
+
if ("3.8.13")
|
|
18932
|
+
return "3.8.13";
|
|
18933
18933
|
} catch {}
|
|
18934
18934
|
const dirsToTry = [];
|
|
18935
18935
|
try {
|
|
@@ -19302,6 +19302,7 @@ var init_posthog = __esm(() => {
|
|
|
19302
19302
|
"agent_conflict_promoted",
|
|
19303
19303
|
"agent_conflict_detected",
|
|
19304
19304
|
"agent_ci_failed_detected",
|
|
19305
|
+
"agent_pr_tracker_bailed",
|
|
19305
19306
|
"agent_prepare_failed",
|
|
19306
19307
|
"agent_worker_spawned",
|
|
19307
19308
|
"agent_worker_exited",
|
|
@@ -59875,8 +59876,6 @@ class OpenSpecChangeStore {
|
|
|
59875
59876
|
} catch {}
|
|
59876
59877
|
}
|
|
59877
59878
|
const changesDir = join7("openspec", "changes");
|
|
59878
|
-
if (!await Bun.file(changesDir).exists())
|
|
59879
|
-
return [];
|
|
59880
59879
|
try {
|
|
59881
59880
|
const entries = await readdir(changesDir, { withFileTypes: true });
|
|
59882
59881
|
return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").map((entry) => entry.name);
|
|
@@ -64364,6 +64363,7 @@ var init_types2 = __esm(() => {
|
|
|
64364
64363
|
model: exports_external.string().default("opus"),
|
|
64365
64364
|
manualTest: exports_external.boolean().default(false),
|
|
64366
64365
|
createPr: exports_external.boolean().default(false),
|
|
64366
|
+
prDraft: exports_external.boolean().default(false),
|
|
64367
64367
|
validateOnComplete: exports_external.boolean().default(false),
|
|
64368
64368
|
usage: UsageSchema.default({}),
|
|
64369
64369
|
history: exports_external.array(HistoryEntrySchema).default([]),
|
|
@@ -64503,6 +64503,7 @@ function buildInitialState(options) {
|
|
|
64503
64503
|
model: options.model ?? "opus",
|
|
64504
64504
|
manualTest: options.manualTest ?? false,
|
|
64505
64505
|
createPr: options.createPr ?? false,
|
|
64506
|
+
prDraft: options.prDraft ?? false,
|
|
64506
64507
|
createdAt: now2,
|
|
64507
64508
|
lastModified: now2,
|
|
64508
64509
|
metadata: { branch }
|
|
@@ -71298,7 +71299,8 @@ When all tasks are complete and all files are committed, push your branch and op
|
|
|
71298
71299
|
`;
|
|
71299
71300
|
prompt += ` git push -u origin HEAD
|
|
71300
71301
|
`;
|
|
71301
|
-
|
|
71302
|
+
const draftFlag = state.prDraft ? " --draft" : "";
|
|
71303
|
+
prompt += ` gh pr create${draftFlag} --title "${state.name}" --body "Summary of changes for ${state.name}"
|
|
71302
71304
|
`;
|
|
71303
71305
|
prompt += `Use the change name as the PR title and write a concise summary of the implementation in the body.
|
|
71304
71306
|
`;
|
|
@@ -93558,6 +93560,7 @@ var init_schema2 = __esm(() => {
|
|
|
93558
93560
|
teardownScript: exports_external2.string().optional(),
|
|
93559
93561
|
appendPrompt: exports_external2.string().optional(),
|
|
93560
93562
|
createPrOnSuccess: exports_external2.boolean().default(false),
|
|
93563
|
+
prDraft: exports_external2.boolean().default(false),
|
|
93561
93564
|
prBaseBranch: exports_external2.string().default("main"),
|
|
93562
93565
|
stackPrsOnDependencies: exports_external2.boolean().default(false),
|
|
93563
93566
|
autoMergeStrategy: exports_external2.enum(["squash", "merge", "rebase"]).default("squash"),
|
|
@@ -93645,6 +93648,15 @@ var init_schema2 = __esm(() => {
|
|
|
93645
93648
|
label: "ralph:pre-existing-error",
|
|
93646
93649
|
outputCharLimit: 4000
|
|
93647
93650
|
}),
|
|
93651
|
+
prTracker: exports_external2.object({
|
|
93652
|
+
enabled: exports_external2.boolean().default(true),
|
|
93653
|
+
maxRecoveryAttempts: exports_external2.number().int().positive().default(3),
|
|
93654
|
+
advanceMergedToDone: exports_external2.boolean().default(false)
|
|
93655
|
+
}).strict().default({
|
|
93656
|
+
enabled: true,
|
|
93657
|
+
maxRecoveryAttempts: 3,
|
|
93658
|
+
advanceMergedToDone: false
|
|
93659
|
+
}),
|
|
93648
93660
|
openspec: exports_external2.object({
|
|
93649
93661
|
reviewPhase: exports_external2.object({
|
|
93650
93662
|
enabled: exports_external2.boolean().default(false),
|
|
@@ -94692,6 +94704,12 @@ async function parseAgentArgs(argv) {
|
|
|
94692
94704
|
case "--no-tmux":
|
|
94693
94705
|
result2.noTmux = true;
|
|
94694
94706
|
break;
|
|
94707
|
+
case "--no-pr-tracker":
|
|
94708
|
+
result2.prTrackerEnabled = false;
|
|
94709
|
+
break;
|
|
94710
|
+
case "--pr-tracker":
|
|
94711
|
+
result2.prTrackerEnabled = true;
|
|
94712
|
+
break;
|
|
94695
94713
|
default:
|
|
94696
94714
|
if (VALID_MODES2.has(arg)) {
|
|
94697
94715
|
result2.mode = arg;
|
|
@@ -94701,6 +94719,12 @@ async function parseAgentArgs(argv) {
|
|
|
94701
94719
|
break;
|
|
94702
94720
|
}
|
|
94703
94721
|
}
|
|
94722
|
+
if (result2.fixCi && !result2.createPr) {
|
|
94723
|
+
throw new Error("--fix-ci requires --create-pr");
|
|
94724
|
+
}
|
|
94725
|
+
if (result2.stackPrs && !result2.createPr) {
|
|
94726
|
+
throw new Error("--stack-prs requires --create-pr");
|
|
94727
|
+
}
|
|
94704
94728
|
return result2;
|
|
94705
94729
|
}
|
|
94706
94730
|
var VALID_MODES2, INDICATOR_KEYS, GET_KEYS, HELP_TEXT2;
|
|
@@ -94769,6 +94793,7 @@ var init_cli2 = __esm(() => {
|
|
|
94769
94793
|
" --code-review Watch open tracked PRs for unresolved review comments",
|
|
94770
94794
|
" --max-tickets <n> Stop picking up new issues after N have been started (0 = unlimited)",
|
|
94771
94795
|
" --no-tmux Disable tmux session management; run agent in the foreground directly",
|
|
94796
|
+
" --no-pr-tracker Disable RLF-173 pr-tracker bail / recovery counter for this run",
|
|
94772
94797
|
" --json-output Emit JSONL to stdout instead of the Ink dashboard (for scripting/CI)",
|
|
94773
94798
|
" (auto-enabled when stdin is not a TTY, e.g. pipes / nohup / CI)",
|
|
94774
94799
|
" --json-log-file <path> Mirror JSONL events to a file (works alongside TUI or --json-output)",
|
|
@@ -96399,9 +96424,18 @@ class AgentCoordinator {
|
|
|
96399
96424
|
counts.conflicted += 1;
|
|
96400
96425
|
else if (pr.status === "ci_failed")
|
|
96401
96426
|
counts.ciFailed += 1;
|
|
96427
|
+
if (pr.status === "mergeable" && this.opts.prTracker) {
|
|
96428
|
+
try {
|
|
96429
|
+
await this.opts.prTracker.clear(issue2.identifier);
|
|
96430
|
+
} catch (err) {
|
|
96431
|
+
this.deps.onLog(`! pr-tracker clear failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96432
|
+
}
|
|
96433
|
+
}
|
|
96402
96434
|
if (pr.status === "conflicted") {
|
|
96403
96435
|
if (this.conflictNotified.has(issue2.id))
|
|
96404
96436
|
continue;
|
|
96437
|
+
if (await this.prTrackerBail(issue2, pr.url, "conflicting"))
|
|
96438
|
+
continue;
|
|
96405
96439
|
emitCapture(this.bus, "agent_conflict_detected", { issue_identifier: issue2.identifier });
|
|
96406
96440
|
this.conflictNotified.add(issue2.id);
|
|
96407
96441
|
this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} conflicting \u2014 queued (conflict-fix)`, "yellow");
|
|
@@ -96422,6 +96456,8 @@ class AgentCoordinator {
|
|
|
96422
96456
|
if (pr.status === "ci_failed") {
|
|
96423
96457
|
if (this.ciFailedNotified.has(issue2.id))
|
|
96424
96458
|
continue;
|
|
96459
|
+
if (await this.prTrackerBail(issue2, pr.url, "ci_failed"))
|
|
96460
|
+
continue;
|
|
96425
96461
|
emitCapture(this.bus, "agent_ci_failed_detected", { issue_identifier: issue2.identifier });
|
|
96426
96462
|
this.ciFailedNotified.add(issue2.id);
|
|
96427
96463
|
this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} CI failing \u2014 queued (ci-fix)`, "yellow");
|
|
@@ -96441,6 +96477,44 @@ class AgentCoordinator {
|
|
|
96441
96477
|
}
|
|
96442
96478
|
return counts;
|
|
96443
96479
|
}
|
|
96480
|
+
async prTrackerBail(issue2, prUrl, reason) {
|
|
96481
|
+
const tracker = this.opts.prTracker;
|
|
96482
|
+
if (!tracker)
|
|
96483
|
+
return false;
|
|
96484
|
+
let decision;
|
|
96485
|
+
try {
|
|
96486
|
+
decision = await tracker.recordFailure(issue2.identifier, reason);
|
|
96487
|
+
} catch (err) {
|
|
96488
|
+
this.deps.onLog(`! pr-tracker record failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96489
|
+
return false;
|
|
96490
|
+
}
|
|
96491
|
+
if (decision.kind === "demote")
|
|
96492
|
+
return false;
|
|
96493
|
+
if (decision.firstBail) {
|
|
96494
|
+
this.deps.onLog(` ${issue2.identifier}: pr-tracker bailing after ${decision.attempts} recovery attempts (${reason}) \u2014 applying setError`, "red");
|
|
96495
|
+
emitCapture(this.bus, "agent_pr_tracker_bailed", {
|
|
96496
|
+
issue_identifier: issue2.identifier,
|
|
96497
|
+
reason,
|
|
96498
|
+
attempts: decision.attempts
|
|
96499
|
+
});
|
|
96500
|
+
if (this.opts.setError) {
|
|
96501
|
+
try {
|
|
96502
|
+
await this.deps.applyIndicator(issue2, this.opts.setError);
|
|
96503
|
+
} catch (err) {
|
|
96504
|
+
this.deps.onLog(`! Linear setError failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96505
|
+
}
|
|
96506
|
+
}
|
|
96507
|
+
if (this.opts.postComments !== false) {
|
|
96508
|
+
const human = reason === "conflicting" ? "merge conflicts" : "failing CI";
|
|
96509
|
+
try {
|
|
96510
|
+
await this.deps.postComment(issue2, `\u274C Ralph gave up auto-recovering this PR (${prUrl}) after ${decision.attempts} attempts \u2014 last failure: ${human}. The \`ralph:error\` label has been applied; clear it (or merge the PR) once a human has looked at it.`);
|
|
96511
|
+
} catch (err) {
|
|
96512
|
+
this.deps.onLog(`! Linear bail comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96513
|
+
}
|
|
96514
|
+
}
|
|
96515
|
+
}
|
|
96516
|
+
return true;
|
|
96517
|
+
}
|
|
96444
96518
|
spawnNext() {
|
|
96445
96519
|
if (this.stopped)
|
|
96446
96520
|
return;
|
|
@@ -98204,17 +98278,20 @@ function createPrepareHelpers(input) {
|
|
|
98204
98278
|
`The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
|
|
98205
98279
|
"",
|
|
98206
98280
|
"Steps:",
|
|
98207
|
-
`1. \`git fetch origin ${cfg.prBaseBranch}\` then
|
|
98281
|
+
`1. \`git fetch origin ${cfg.prBaseBranch}\` then merge \`${cfg.prBaseBranch}\` into the current branch (\`git merge origin/${cfg.prBaseBranch}\`). Do NOT rebase.`,
|
|
98208
98282
|
"2. Resolve conflicts in the files git lists.",
|
|
98209
|
-
"3. Stage and commit the resolution.",
|
|
98210
|
-
`4. Push the resolved branch with \`git push
|
|
98283
|
+
"3. Stage and commit the resolution as a new merge commit. Do NOT amend existing commits.",
|
|
98284
|
+
`4. Push the resolved branch with \`git push origin ${branchRef}\`. Never force-push.`,
|
|
98211
98285
|
` The post-task harness will NOT push for you in conflict-fix mode \u2014 you own the push.`,
|
|
98212
98286
|
` If the push is rejected, inspect the rejection output and react inline before retrying:`,
|
|
98213
|
-
` - **
|
|
98214
|
-
` \`git fetch origin ${branchRef}\` then
|
|
98215
|
-
` conflicts, and retry the push.`,
|
|
98287
|
+
` - **non-fast-forward** (someone else pushed to \`${branchRef}\`):`,
|
|
98288
|
+
` \`git fetch origin ${branchRef}\` then \`git merge origin/${branchRef}\` to bring their`,
|
|
98289
|
+
` changes in as a new merge commit, re-resolve any new conflicts, and retry the push.`,
|
|
98290
|
+
` Do NOT rebase and do NOT \`--force\` / \`--force-with-lease\` \u2014 work on the remote must`,
|
|
98291
|
+
` never be overwritten.`,
|
|
98216
98292
|
` - **pre-push hook failure** (lint, typecheck, tests): fix the underlying problem locally,`,
|
|
98217
|
-
` \`git add\` + \`git commit
|
|
98293
|
+
` \`git add\` + \`git commit\` as a new commit (NEVER \`--amend\` an existing commit),`,
|
|
98294
|
+
` then retry the push.`,
|
|
98218
98295
|
` - **ref-update policy rejection** (branch protection, required reviews): log the rejection`,
|
|
98219
98296
|
` message and stop \u2014 this requires human intervention; do not force past it.`,
|
|
98220
98297
|
` Only stop after exhausting the in-context fix. The push must succeed before this iteration ends.`,
|
|
@@ -98247,7 +98324,8 @@ PR: ${prUrl}` : ""
|
|
|
98247
98324
|
`2. Fix the underlying failures in the worktree (tests, lint, typecheck, build).`,
|
|
98248
98325
|
`3. Stage and commit the fixes.`,
|
|
98249
98326
|
`4. Push with \`git push origin ${ciBranchRef}\`. If the push is rejected as`,
|
|
98250
|
-
` non-fast-forward, \`git fetch origin ${ciBranchRef}\` then
|
|
98327
|
+
` non-fast-forward, \`git fetch origin ${ciBranchRef}\` then \`git merge origin/${ciBranchRef}\``,
|
|
98328
|
+
` before retrying. Do NOT rebase, do NOT amend, and never force-push.`,
|
|
98251
98329
|
`5. Wait for CI to re-run; if checks are still red, repeat from step 1.`,
|
|
98252
98330
|
` Stop only when CI is green or when the failure is clearly outside the change's scope`,
|
|
98253
98331
|
` (flaky infra, external service down) \u2014 in that case, log the rejection and exit.`,
|
|
@@ -99171,7 +99249,10 @@ async function createPullRequest(input, runner) {
|
|
|
99171
99249
|
return { url: existingUrl, created: false };
|
|
99172
99250
|
const title = defaultTitle(input.issue);
|
|
99173
99251
|
const body = defaultBody(input.issue, input.branch);
|
|
99174
|
-
const
|
|
99252
|
+
const createArgs = ["gh", "pr", "create", "--base", base2, "--title", title, "--body", body];
|
|
99253
|
+
if (input.draft)
|
|
99254
|
+
createArgs.push("--draft");
|
|
99255
|
+
const created = await runner.run(createArgs, input.cwd);
|
|
99175
99256
|
const url2 = created.stdout.trim().split(`
|
|
99176
99257
|
`).pop() ?? "";
|
|
99177
99258
|
return { url: url2, created: true };
|
|
@@ -99326,9 +99407,37 @@ async function runWorkerWithFixTask(ctx, heading, body) {
|
|
|
99326
99407
|
return 1;
|
|
99327
99408
|
}
|
|
99328
99409
|
await reactivateState(ctx.stateFilePath, ctx.log, ctx.changeName);
|
|
99329
|
-
|
|
99410
|
+
let preHead = "";
|
|
99411
|
+
try {
|
|
99412
|
+
const r = await ctx.cmd.run(["git", "rev-parse", "HEAD"], ctx.cwd);
|
|
99413
|
+
preHead = r.stdout.trim();
|
|
99414
|
+
} catch (err) {
|
|
99415
|
+
ctx.log(`! could not snapshot HEAD before fix task: ${err.message}`, "yellow");
|
|
99416
|
+
}
|
|
99417
|
+
const code = await ctx.respawnWorker();
|
|
99418
|
+
if (preHead) {
|
|
99419
|
+
try {
|
|
99420
|
+
const r = await ctx.cmd.run(["git", "rev-parse", "HEAD"], ctx.cwd);
|
|
99421
|
+
const postHead = r.stdout.trim();
|
|
99422
|
+
if (postHead !== preHead) {
|
|
99423
|
+
let isAncestor = true;
|
|
99424
|
+
try {
|
|
99425
|
+
await ctx.cmd.run(["git", "merge-base", "--is-ancestor", preHead, postHead], ctx.cwd);
|
|
99426
|
+
} catch {
|
|
99427
|
+
isAncestor = false;
|
|
99428
|
+
}
|
|
99429
|
+
if (!isAncestor) {
|
|
99430
|
+
ctx.log(`! fix worker for "${heading}" rewrote history \u2014 pre=${preHead.slice(0, 8)} ` + `is not an ancestor of post=${postHead.slice(0, 8)}. Aborting and preserving ` + `worktree at ${ctx.cwd}.`, "red");
|
|
99431
|
+
return 1;
|
|
99432
|
+
}
|
|
99433
|
+
}
|
|
99434
|
+
} catch (err) {
|
|
99435
|
+
ctx.log(`! could not verify append-only history after fix task: ${err.message}`, "yellow");
|
|
99436
|
+
}
|
|
99437
|
+
}
|
|
99438
|
+
return code;
|
|
99330
99439
|
}
|
|
99331
|
-
async function
|
|
99440
|
+
async function pushBranchSafely(ctx) {
|
|
99332
99441
|
try {
|
|
99333
99442
|
ctx.emit("pushing", "after conflict resolution");
|
|
99334
99443
|
await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
|
|
@@ -99342,10 +99451,12 @@ ${pe.stderr ?? ""}`;
|
|
|
99342
99451
|
return false;
|
|
99343
99452
|
}
|
|
99344
99453
|
try {
|
|
99345
|
-
await ctx.cmd.run(["git", "
|
|
99454
|
+
await ctx.cmd.run(["git", "fetch", "origin", ctx.branch], ctx.cwd);
|
|
99455
|
+
await ctx.cmd.run(["git", "merge", "--no-edit", `origin/${ctx.branch}`], ctx.cwd);
|
|
99456
|
+
await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
|
|
99346
99457
|
return true;
|
|
99347
|
-
} catch (
|
|
99348
|
-
ctx.log(`!
|
|
99458
|
+
} catch (retryErr) {
|
|
99459
|
+
ctx.log(`! push after merging origin/${ctx.branch} failed: ${retryErr.message}`, "red");
|
|
99349
99460
|
return false;
|
|
99350
99461
|
}
|
|
99351
99462
|
}
|
|
@@ -99364,7 +99475,8 @@ async function createPrWithRetry(ctx, issue2) {
|
|
|
99364
99475
|
branch: ctx.branch,
|
|
99365
99476
|
issue: issue2,
|
|
99366
99477
|
base: base2,
|
|
99367
|
-
metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? []
|
|
99478
|
+
metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? [],
|
|
99479
|
+
draft: ctx.cfg.prDraft ?? false
|
|
99368
99480
|
}, ctx.cmd);
|
|
99369
99481
|
return { pr: pr2, gaveUp: false };
|
|
99370
99482
|
} catch (err) {
|
|
@@ -99377,26 +99489,26 @@ ${e.stderr ?? ""}`;
|
|
|
99377
99489
|
const pushRejected = isHookReject || /failed to push some refs/i.test(combined);
|
|
99378
99490
|
if (isNonFastForward && !nonFfRebaseAttempted) {
|
|
99379
99491
|
nonFfRebaseAttempted = true;
|
|
99380
|
-
ctx.emit("
|
|
99381
|
-
ctx.log(` non-fast-forward push for ${ctx.changeName} \u2014
|
|
99492
|
+
ctx.emit("merging", `git pull --no-rebase origin ${ctx.branch}`);
|
|
99493
|
+
ctx.log(` non-fast-forward push for ${ctx.changeName} \u2014 merging origin/${ctx.branch} into the branch`, "yellow");
|
|
99382
99494
|
try {
|
|
99383
99495
|
await ctx.cmd.run(["git", "fetch", "origin", ctx.branch], ctx.cwd);
|
|
99384
|
-
await ctx.cmd.run(["git", "pull", "--rebase", "origin", ctx.branch], ctx.cwd);
|
|
99496
|
+
await ctx.cmd.run(["git", "pull", "--no-rebase", "--autostash", "--no-edit", "origin", ctx.branch], ctx.cwd);
|
|
99385
99497
|
continue;
|
|
99386
|
-
} catch (
|
|
99387
|
-
const re =
|
|
99498
|
+
} catch (mergeErr) {
|
|
99499
|
+
const re = mergeErr;
|
|
99388
99500
|
const reBlob = `${re.stdout ?? ""}
|
|
99389
99501
|
${re.stderr ?? ""}`;
|
|
99390
|
-
const isConflict = /CONFLICT|Merge conflict|
|
|
99502
|
+
const isConflict = /CONFLICT|Merge conflict|both modified/i.test(reBlob);
|
|
99391
99503
|
if (!isConflict) {
|
|
99392
|
-
ctx.log(`!
|
|
99504
|
+
ctx.log(`! merge failed for ${ctx.changeName}: ${mergeErr.message} \u2014 giving up`, "red");
|
|
99393
99505
|
return { pr: null, gaveUp: true };
|
|
99394
99506
|
}
|
|
99395
|
-
ctx.emit("
|
|
99507
|
+
ctx.emit("merging", "conflicts detected \u2014 aborting + queueing fix task");
|
|
99396
99508
|
try {
|
|
99397
|
-
await ctx.cmd.run(["git", "
|
|
99509
|
+
await ctx.cmd.run(["git", "merge", "--abort"], ctx.cwd);
|
|
99398
99510
|
} catch (err2) {
|
|
99399
|
-
ctx.log(`! git
|
|
99511
|
+
ctx.log(`! git merge --abort failed (worktree may already be clean): ${err2.message}`, "yellow");
|
|
99400
99512
|
}
|
|
99401
99513
|
let conflictedFiles = "";
|
|
99402
99514
|
try {
|
|
@@ -99406,23 +99518,23 @@ ${re.stderr ?? ""}`;
|
|
|
99406
99518
|
ctx.log(`! could not list conflicted files: ${err2.message}`, "yellow");
|
|
99407
99519
|
}
|
|
99408
99520
|
if (hookFixAttempt >= maxAttempts) {
|
|
99409
|
-
ctx.log(`! merge conflict
|
|
99521
|
+
ctx.log(`! merge conflict merging origin/${ctx.branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${ctx.cwd}`, "red");
|
|
99410
99522
|
ctx.log(` detail: ${reBlob.trim().split(`
|
|
99411
99523
|
`).slice(0, 8).join(`
|
|
99412
99524
|
`)}`, "red");
|
|
99413
99525
|
return { pr: null, gaveUp: true };
|
|
99414
99526
|
}
|
|
99415
99527
|
hookFixAttempt += 1;
|
|
99416
|
-
ctx.emit("
|
|
99417
|
-
ctx.log(`! merge conflict
|
|
99418
|
-
const retryCode2 = await runWorkerWithFixTask(ctx, "Resolve merge conflict with origin/" + ctx.branch, `Push to origin/${ctx.branch} was rejected as non-fast-forward, and
|
|
99528
|
+
ctx.emit("merging", `conflict-fix ${hookFixAttempt}/${maxAttempts}`);
|
|
99529
|
+
ctx.log(`! merge conflict merging origin/${ctx.branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxAttempts})`, "yellow");
|
|
99530
|
+
const retryCode2 = await runWorkerWithFixTask(ctx, "Resolve merge conflict with origin/" + ctx.branch, `Push to origin/${ctx.branch} was rejected as non-fast-forward, and merging ` + `origin/${ctx.branch} into the branch produced merge conflicts.
|
|
99419
99531
|
|
|
99420
|
-
` + `Run \`git fetch origin ${ctx.branch}\` and \`git
|
|
99532
|
+
` + `Run \`git fetch origin ${ctx.branch}\` and \`git merge origin/${ctx.branch}\`, ` + `resolve every conflict, \`git add\` the resolved files, and finish with ` + `\`git commit\` (or \`git merge --continue\`). Do NOT rebase and do NOT ` + `amend existing commits \u2014 only add new commits. The push will be retried ` + `after this loop iteration finishes.
|
|
99421
99533
|
|
|
99422
99534
|
` + (conflictedFiles ? `Files that differ between your branch and origin/${ctx.branch}:
|
|
99423
99535
|
${conflictedFiles}
|
|
99424
99536
|
|
|
99425
|
-
` : "") + `
|
|
99537
|
+
` : "") + `Merge output:
|
|
99426
99538
|
${reBlob.trim()}`);
|
|
99427
99539
|
if (retryCode2 !== 0) {
|
|
99428
99540
|
ctx.log(`! worker re-run after merge conflict exited code ${retryCode2} \u2014 giving up`, "red");
|
|
@@ -99447,6 +99559,8 @@ ${reBlob.trim()}`);
|
|
|
99447
99559
|
ctx.log(` detail: ${detail}`, "yellow");
|
|
99448
99560
|
const retryCode = await runWorkerWithFixTask(ctx, "Fix push rejection", `Push to origin/${ctx.branch} was rejected. Fix the underlying problem ` + `(e.g. failing pre-push hook checks), then the push will be retried.
|
|
99449
99561
|
|
|
99562
|
+
` + `Do NOT delete, revert, amend, rebase, reorder, or squash existing commits. ` + `Only add new commits or edit working-tree files. If a pre-push check (test, ` + `lint, typecheck, etc.) fails on the change you just made, fix the test or ` + `the code under test \u2014 do not remove the change to silence the failure.
|
|
99563
|
+
|
|
99450
99564
|
` + combined.trim());
|
|
99451
99565
|
if (retryCode !== 0) {
|
|
99452
99566
|
ctx.log(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
|
|
@@ -99480,16 +99594,16 @@ async function fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict) {
|
|
|
99480
99594
|
`The PR ${prUrl} has merge conflicts with \`${ctx.base}\`.`,
|
|
99481
99595
|
"",
|
|
99482
99596
|
"Steps:",
|
|
99483
|
-
`1. \`git fetch origin ${ctx.base}\` then
|
|
99597
|
+
`1. \`git fetch origin ${ctx.base}\` then merge \`${ctx.base}\` into the current branch (\`git merge origin/${ctx.base}\`). Do NOT rebase and do NOT amend existing commits.`,
|
|
99484
99598
|
"2. Resolve conflicts in the files git lists.",
|
|
99485
|
-
"3. Stage and commit the resolution."
|
|
99599
|
+
"3. Stage and commit the resolution as a new merge commit."
|
|
99486
99600
|
].join(`
|
|
99487
99601
|
`));
|
|
99488
99602
|
if (conflictCode !== 0) {
|
|
99489
99603
|
ctx.log(`! conflict resolution worker exited code ${conflictCode} \u2014 giving up`, "red");
|
|
99490
99604
|
return PR_FAILED_EXIT;
|
|
99491
99605
|
}
|
|
99492
|
-
const pushed = await
|
|
99606
|
+
const pushed = await pushBranchSafely(ctx);
|
|
99493
99607
|
if (!pushed)
|
|
99494
99608
|
return PR_FAILED_EXIT;
|
|
99495
99609
|
continue;
|
|
@@ -99693,7 +99807,8 @@ ${indented}${suffix}`, "yellow");
|
|
|
99693
99807
|
log2(` ${pr2.created ? "opened" : "found existing"} PR: ${prUrl}`, "green");
|
|
99694
99808
|
registerPr?.(changeName, prUrl);
|
|
99695
99809
|
let manualMergePending = false;
|
|
99696
|
-
|
|
99810
|
+
const prReadyNeeded = cfg.prDraft === true;
|
|
99811
|
+
if (!prReadyNeeded && wantAutoMerge) {
|
|
99697
99812
|
const fallbackEnabled = cfg.manualMergeWhenAutoMergeDisabled !== false;
|
|
99698
99813
|
const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log2);
|
|
99699
99814
|
if (repoAllowsAutoMerge === false && fallbackEnabled) {
|
|
@@ -99714,10 +99829,23 @@ ${indented}${suffix}`, "yellow");
|
|
|
99714
99829
|
}
|
|
99715
99830
|
}
|
|
99716
99831
|
}
|
|
99832
|
+
} else if (prReadyNeeded && wantAutoMerge) {
|
|
99833
|
+
manualMergePending = true;
|
|
99717
99834
|
}
|
|
99718
99835
|
const ciResult = await fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict);
|
|
99719
99836
|
if (ciResult !== 0)
|
|
99720
99837
|
return ciResult;
|
|
99838
|
+
if (prReadyNeeded) {
|
|
99839
|
+
emit2("pr-ready");
|
|
99840
|
+
try {
|
|
99841
|
+
await cmd.run(["gh", "pr", "ready", prUrl], cwd2);
|
|
99842
|
+
log2(` converted ${prUrl} from draft to ready`, "green");
|
|
99843
|
+
} catch (err) {
|
|
99844
|
+
const e = err;
|
|
99845
|
+
log2(`! gh pr ready failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
|
|
99846
|
+
manualMergePending = false;
|
|
99847
|
+
}
|
|
99848
|
+
}
|
|
99721
99849
|
if (manualMergePending) {
|
|
99722
99850
|
try {
|
|
99723
99851
|
await cmd.run(["gh", "pr", "merge", prUrl, `--${cfg.autoMergeStrategy}`], cwd2);
|
|
@@ -100203,6 +100331,7 @@ function createSpawnWorker(input) {
|
|
|
100203
100331
|
neverTouch: cfg.boundaries.never_touch,
|
|
100204
100332
|
metaOnlyFiles: cfg.boundaries.meta_only_files,
|
|
100205
100333
|
manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled,
|
|
100334
|
+
prDraft: cfg.prDraft,
|
|
100206
100335
|
validateCommands: [cfg.commands.test, cfg.commands.lint, cfg.commands.typecheck].filter((c) => Boolean(c))
|
|
100207
100336
|
},
|
|
100208
100337
|
respawnWorker: respawn
|
|
@@ -253151,8 +253280,105 @@ var init_comment_sync2 = __esm(() => {
|
|
|
253151
253280
|
init_linear();
|
|
253152
253281
|
});
|
|
253153
253282
|
|
|
253154
|
-
// apps/agent/src/
|
|
253283
|
+
// apps/agent/src/features/pr-tracker/state.ts
|
|
253155
253284
|
import { join as join33 } from "path";
|
|
253285
|
+
async function readState2(projectRoot) {
|
|
253286
|
+
const path = join33(projectRoot, PR_TRACKER_STATE_RELPATH);
|
|
253287
|
+
const file2 = Bun.file(path);
|
|
253288
|
+
if (!await file2.exists())
|
|
253289
|
+
return {};
|
|
253290
|
+
try {
|
|
253291
|
+
const raw = await file2.text();
|
|
253292
|
+
const parsed = JSON.parse(raw);
|
|
253293
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
253294
|
+
return parsed;
|
|
253295
|
+
}
|
|
253296
|
+
return {};
|
|
253297
|
+
} catch {
|
|
253298
|
+
return {};
|
|
253299
|
+
}
|
|
253300
|
+
}
|
|
253301
|
+
async function writeState2(projectRoot, state) {
|
|
253302
|
+
const path = join33(projectRoot, PR_TRACKER_STATE_RELPATH);
|
|
253303
|
+
await Bun.write(path, JSON.stringify(state, null, 2));
|
|
253304
|
+
}
|
|
253305
|
+
var PR_TRACKER_STATE_RELPATH = ".ralph/pr-tracker-state.json";
|
|
253306
|
+
var init_state3 = () => {};
|
|
253307
|
+
|
|
253308
|
+
// apps/agent/src/features/pr-tracker/tracker.ts
|
|
253309
|
+
class PrTracker {
|
|
253310
|
+
opts;
|
|
253311
|
+
state = {};
|
|
253312
|
+
loaded = false;
|
|
253313
|
+
now;
|
|
253314
|
+
constructor(opts) {
|
|
253315
|
+
this.opts = opts;
|
|
253316
|
+
this.now = opts.now ?? (() => new Date);
|
|
253317
|
+
}
|
|
253318
|
+
async load() {
|
|
253319
|
+
if (this.loaded)
|
|
253320
|
+
return;
|
|
253321
|
+
this.state = await readState2(this.opts.projectRoot);
|
|
253322
|
+
this.loaded = true;
|
|
253323
|
+
}
|
|
253324
|
+
snapshot() {
|
|
253325
|
+
return JSON.parse(JSON.stringify(this.state));
|
|
253326
|
+
}
|
|
253327
|
+
isBailed(identifier) {
|
|
253328
|
+
return this.state[identifier]?.bailed === true;
|
|
253329
|
+
}
|
|
253330
|
+
getAttempts(identifier) {
|
|
253331
|
+
return this.state[identifier]?.attempts ?? 0;
|
|
253332
|
+
}
|
|
253333
|
+
async recordFailure(identifier, reason) {
|
|
253334
|
+
await this.load();
|
|
253335
|
+
const nowIso = this.now().toISOString();
|
|
253336
|
+
const existing = this.state[identifier];
|
|
253337
|
+
if (existing?.bailed) {
|
|
253338
|
+
existing.lastReason = reason;
|
|
253339
|
+
await this.flush();
|
|
253340
|
+
return { kind: "bail", attempts: existing.attempts, firstBail: false };
|
|
253341
|
+
}
|
|
253342
|
+
const attempts = (existing?.attempts ?? 0) + 1;
|
|
253343
|
+
const entry = {
|
|
253344
|
+
attempts,
|
|
253345
|
+
firstFailedAt: existing?.firstFailedAt ?? nowIso,
|
|
253346
|
+
lastDemotedAt: nowIso,
|
|
253347
|
+
lastReason: reason
|
|
253348
|
+
};
|
|
253349
|
+
if (attempts >= this.opts.maxRecoveryAttempts) {
|
|
253350
|
+
entry.bailed = true;
|
|
253351
|
+
this.state[identifier] = entry;
|
|
253352
|
+
await this.flush();
|
|
253353
|
+
return { kind: "bail", attempts, firstBail: true };
|
|
253354
|
+
}
|
|
253355
|
+
this.state[identifier] = entry;
|
|
253356
|
+
await this.flush();
|
|
253357
|
+
return { kind: "demote", attempts };
|
|
253358
|
+
}
|
|
253359
|
+
async clear(identifier) {
|
|
253360
|
+
await this.load();
|
|
253361
|
+
if (!(identifier in this.state))
|
|
253362
|
+
return;
|
|
253363
|
+
delete this.state[identifier];
|
|
253364
|
+
await this.flush();
|
|
253365
|
+
}
|
|
253366
|
+
async flush() {
|
|
253367
|
+
await writeState2(this.opts.projectRoot, this.state);
|
|
253368
|
+
}
|
|
253369
|
+
}
|
|
253370
|
+
var init_tracker = __esm(() => {
|
|
253371
|
+
init_state3();
|
|
253372
|
+
});
|
|
253373
|
+
|
|
253374
|
+
// apps/agent/src/features/pr-tracker/index.ts
|
|
253375
|
+
var init_pr_tracker = __esm(() => {
|
|
253376
|
+
init_tracker();
|
|
253377
|
+
init_state3();
|
|
253378
|
+
});
|
|
253379
|
+
|
|
253380
|
+
// apps/agent/src/agent/wire.ts
|
|
253381
|
+
import { join as join34 } from "path";
|
|
253156
253382
|
function buildAgentCoordinator(input) {
|
|
253157
253383
|
const {
|
|
253158
253384
|
args,
|
|
@@ -253171,7 +253397,7 @@ function buildAgentCoordinator(input) {
|
|
|
253171
253397
|
onWorkerCmd,
|
|
253172
253398
|
onAwaitingTicket
|
|
253173
253399
|
} = input;
|
|
253174
|
-
const logsDir =
|
|
253400
|
+
const logsDir = join34(projectRoot, ".ralph", "logs");
|
|
253175
253401
|
const bus = createBus();
|
|
253176
253402
|
subscribeAgentDiag(bus, onLog);
|
|
253177
253403
|
const diag = (area, message, color) => {
|
|
@@ -253325,6 +253551,11 @@ function buildAgentCoordinator(input) {
|
|
|
253325
253551
|
now: () => new Date
|
|
253326
253552
|
};
|
|
253327
253553
|
}
|
|
253554
|
+
const prTrackerEnabled = args.prTrackerEnabled === undefined ? cfg.prTracker.enabled : args.prTrackerEnabled;
|
|
253555
|
+
const prTracker = prTrackerEnabled ? new PrTracker({
|
|
253556
|
+
projectRoot,
|
|
253557
|
+
maxRecoveryAttempts: cfg.prTracker.maxRecoveryAttempts
|
|
253558
|
+
}) : null;
|
|
253328
253559
|
const commentSync = createCommentSyncHooks({
|
|
253329
253560
|
apiKey,
|
|
253330
253561
|
cfg,
|
|
@@ -253339,7 +253570,7 @@ function buildAgentCoordinator(input) {
|
|
|
253339
253570
|
pollContext = new PollContext;
|
|
253340
253571
|
},
|
|
253341
253572
|
fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
|
|
253342
|
-
fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress,
|
|
253573
|
+
fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
|
|
253343
253574
|
fetchReview: () => resolvers.fetchByGet(indicators.getReview, excludeFromReview),
|
|
253344
253575
|
fetchMentions,
|
|
253345
253576
|
fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, indicators),
|
|
@@ -253378,7 +253609,8 @@ function buildAgentCoordinator(input) {
|
|
|
253378
253609
|
...indicators.getAutoMerge !== undefined ? { getAutoMerge: indicators.getAutoMerge } : {},
|
|
253379
253610
|
postComments: cfg.linear.postComments,
|
|
253380
253611
|
commentEveryIterations: cfg.linear.updateEveryIterations,
|
|
253381
|
-
...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {}
|
|
253612
|
+
...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {},
|
|
253613
|
+
...prTracker ? { prTracker } : {}
|
|
253382
253614
|
});
|
|
253383
253615
|
coordRef.current = coord;
|
|
253384
253616
|
const filterDesc = describeIndicators(indicators, team, assignee);
|
|
@@ -253422,6 +253654,7 @@ var init_wire = __esm(() => {
|
|
|
253422
253654
|
init_worker();
|
|
253423
253655
|
init_baseline();
|
|
253424
253656
|
init_comment_sync2();
|
|
253657
|
+
init_pr_tracker();
|
|
253425
253658
|
});
|
|
253426
253659
|
|
|
253427
253660
|
// apps/agent/src/agent/json-log/json-log-file.ts
|
|
@@ -253676,7 +253909,7 @@ var init_output_utils = __esm(() => {
|
|
|
253676
253909
|
});
|
|
253677
253910
|
|
|
253678
253911
|
// apps/agent/src/agent/state/worker-state-poll.ts
|
|
253679
|
-
import { join as
|
|
253912
|
+
import { join as join35 } from "path";
|
|
253680
253913
|
function parseSubtasks(tasksMd) {
|
|
253681
253914
|
const out = [];
|
|
253682
253915
|
let skipSection = false;
|
|
@@ -253709,7 +253942,7 @@ function initialWorkerSnapshot() {
|
|
|
253709
253942
|
async function readWorkerSnapshot(input) {
|
|
253710
253943
|
const next = { ...input.prev };
|
|
253711
253944
|
try {
|
|
253712
|
-
const file2 = Bun.file(
|
|
253945
|
+
const file2 = Bun.file(join35(input.statesDir, input.changeName, ".ralph-state.json"));
|
|
253713
253946
|
if (await file2.exists()) {
|
|
253714
253947
|
const json2 = await file2.json();
|
|
253715
253948
|
next.iter = json2.iteration ?? next.iter;
|
|
@@ -253718,10 +253951,10 @@ async function readWorkerSnapshot(input) {
|
|
|
253718
253951
|
} catch {}
|
|
253719
253952
|
if (input.changeDir) {
|
|
253720
253953
|
try {
|
|
253721
|
-
const tasksFile = Bun.file(
|
|
253722
|
-
const proposalFile = Bun.file(
|
|
253723
|
-
const designFile = Bun.file(
|
|
253724
|
-
const reviewFindingsFile = Bun.file(
|
|
253954
|
+
const tasksFile = Bun.file(join35(input.changeDir, "tasks.md"));
|
|
253955
|
+
const proposalFile = Bun.file(join35(input.changeDir, "proposal.md"));
|
|
253956
|
+
const designFile = Bun.file(join35(input.changeDir, "design.md"));
|
|
253957
|
+
const reviewFindingsFile = Bun.file(join35(input.changeDir, "review-findings.md"));
|
|
253725
253958
|
const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
|
|
253726
253959
|
tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
|
|
253727
253960
|
proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
|
|
@@ -253784,7 +254017,7 @@ var init_worker_state_poll = __esm(() => {
|
|
|
253784
254017
|
});
|
|
253785
254018
|
|
|
253786
254019
|
// apps/agent/src/components/AgentMode.tsx
|
|
253787
|
-
import { join as
|
|
254020
|
+
import { join as join36 } from "path";
|
|
253788
254021
|
async function appendSteeringImpl(changeDir, message) {
|
|
253789
254022
|
await runWithContext(createDefaultContext(), async () => {
|
|
253790
254023
|
appendSteeringMessage(changeDir, message);
|
|
@@ -253801,6 +254034,16 @@ function orderSubtasksForCappedDisplay(subtasks) {
|
|
|
253801
254034
|
(s.done ? done : pending).push(s);
|
|
253802
254035
|
return [...pending, ...done];
|
|
253803
254036
|
}
|
|
254037
|
+
function pickLatestGatedTicket(tickets) {
|
|
254038
|
+
if (tickets.size === 0)
|
|
254039
|
+
return { top: null, moreCount: 0 };
|
|
254040
|
+
const sorted = Array.from(tickets.entries()).sort(([, a], [, b2]) => {
|
|
254041
|
+
const aTime = a.since ? new Date(a.since).getTime() : 0;
|
|
254042
|
+
const bTime = b2.since ? new Date(b2.since).getTime() : 0;
|
|
254043
|
+
return bTime - aTime;
|
|
254044
|
+
});
|
|
254045
|
+
return { top: sorted[0], moreCount: sorted.length - 1 };
|
|
254046
|
+
}
|
|
253804
254047
|
function fmtCmd(argv) {
|
|
253805
254048
|
const joined = argv.join(" ");
|
|
253806
254049
|
return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
|
|
@@ -254795,7 +255038,11 @@ function AgentMode({
|
|
|
254795
255038
|
})
|
|
254796
255039
|
}, undefined, false, undefined, this)
|
|
254797
255040
|
}, undefined, false, undefined, this),
|
|
254798
|
-
|
|
255041
|
+
(() => {
|
|
255042
|
+
const { top, moreCount } = pickLatestGatedTicket(gatedTicketsRef.current);
|
|
255043
|
+
if (!top)
|
|
255044
|
+
return null;
|
|
255045
|
+
const [changeName, g2] = top;
|
|
254799
255046
|
const askedAgo = g2.since ? fmtElapsed(now2 - Date.parse(g2.since)) : "just now";
|
|
254800
255047
|
const cardLabelWidth = g2.issueIdentifier.length + 2;
|
|
254801
255048
|
const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(jsx_dev_runtime10.Fragment, {
|
|
@@ -254815,63 +255062,71 @@ function AgentMode({
|
|
|
254815
255062
|
}, undefined, false, undefined, this)
|
|
254816
255063
|
]
|
|
254817
255064
|
}, undefined, true, undefined, this);
|
|
254818
|
-
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(
|
|
254819
|
-
labelNode: cardLabelNode,
|
|
254820
|
-
labelVisualWidth: cardLabelWidth,
|
|
254821
|
-
borderColor: "yellow",
|
|
254822
|
-
paddingX: 1,
|
|
254823
|
-
gap: 2,
|
|
254824
|
-
width: termWidth,
|
|
255065
|
+
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(jsx_dev_runtime10.Fragment, {
|
|
254825
255066
|
children: [
|
|
254826
|
-
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(
|
|
254827
|
-
|
|
254828
|
-
|
|
254829
|
-
|
|
254830
|
-
|
|
254831
|
-
|
|
254832
|
-
|
|
254833
|
-
children:
|
|
254834
|
-
|
|
254835
|
-
|
|
254836
|
-
|
|
254837
|
-
|
|
254838
|
-
|
|
254839
|
-
|
|
254840
|
-
|
|
254841
|
-
|
|
254842
|
-
|
|
254843
|
-
|
|
254844
|
-
|
|
254845
|
-
|
|
254846
|
-
|
|
254847
|
-
|
|
254848
|
-
|
|
254849
|
-
|
|
254850
|
-
|
|
254851
|
-
|
|
254852
|
-
|
|
254853
|
-
|
|
254854
|
-
|
|
254855
|
-
|
|
254856
|
-
|
|
254857
|
-
|
|
254858
|
-
|
|
254859
|
-
|
|
254860
|
-
|
|
254861
|
-
|
|
254862
|
-
|
|
254863
|
-
|
|
254864
|
-
|
|
254865
|
-
|
|
254866
|
-
|
|
254867
|
-
|
|
254868
|
-
|
|
255067
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(LabeledBox, {
|
|
255068
|
+
labelNode: cardLabelNode,
|
|
255069
|
+
labelVisualWidth: cardLabelWidth,
|
|
255070
|
+
borderColor: "yellow",
|
|
255071
|
+
paddingX: 1,
|
|
255072
|
+
gap: 2,
|
|
255073
|
+
width: termWidth,
|
|
255074
|
+
children: [
|
|
255075
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255076
|
+
color: "yellow",
|
|
255077
|
+
bold: true,
|
|
255078
|
+
children: "[GATE]"
|
|
255079
|
+
}, undefined, false, undefined, this),
|
|
255080
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255081
|
+
color: "yellow",
|
|
255082
|
+
children: "Awaiting confirmation"
|
|
255083
|
+
}, undefined, false, undefined, this),
|
|
255084
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255085
|
+
dimColor: true,
|
|
255086
|
+
children: "\xB7"
|
|
255087
|
+
}, undefined, false, undefined, this),
|
|
255088
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255089
|
+
dimColor: true,
|
|
255090
|
+
children: "round"
|
|
255091
|
+
}, undefined, false, undefined, this),
|
|
255092
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255093
|
+
color: "white",
|
|
255094
|
+
bold: true,
|
|
255095
|
+
children: g2.round
|
|
255096
|
+
}, undefined, false, undefined, this),
|
|
255097
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255098
|
+
dimColor: true,
|
|
255099
|
+
children: "\xB7"
|
|
255100
|
+
}, undefined, false, undefined, this),
|
|
255101
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255102
|
+
dimColor: true,
|
|
255103
|
+
children: "asked"
|
|
255104
|
+
}, undefined, false, undefined, this),
|
|
255105
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255106
|
+
color: "white",
|
|
255107
|
+
children: askedAgo
|
|
255108
|
+
}, undefined, false, undefined, this),
|
|
255109
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255110
|
+
dimColor: true,
|
|
255111
|
+
children: "ago"
|
|
255112
|
+
}, undefined, false, undefined, this),
|
|
255113
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255114
|
+
dimColor: true,
|
|
255115
|
+
children: "\u2502"
|
|
255116
|
+
}, undefined, false, undefined, this),
|
|
255117
|
+
/* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
255118
|
+
dimColor: true,
|
|
255119
|
+
children: trunc(g2.issueTitle, Math.max(20, termWidth - 70))
|
|
255120
|
+
}, undefined, false, undefined, this)
|
|
255121
|
+
]
|
|
255122
|
+
}, `gated-${changeName}`, true, undefined, this),
|
|
255123
|
+
moreCount > 0 && /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
|
|
254869
255124
|
dimColor: true,
|
|
254870
|
-
children:
|
|
255125
|
+
children: ` +${moreCount} more awaiting confirmation`
|
|
254871
255126
|
}, undefined, false, undefined, this)
|
|
254872
255127
|
]
|
|
254873
|
-
},
|
|
254874
|
-
}),
|
|
255128
|
+
}, undefined, true, undefined, this);
|
|
255129
|
+
})(),
|
|
254875
255130
|
coord?.activeWorkers.map((w2, idx) => {
|
|
254876
255131
|
const isFocused = idx === safeFocusedIdx;
|
|
254877
255132
|
const meta3 = workerMetaRef.current.get(w2.changeName);
|
|
@@ -255245,7 +255500,7 @@ function AgentMode({
|
|
|
255245
255500
|
},
|
|
255246
255501
|
onSubmit: async (message) => {
|
|
255247
255502
|
try {
|
|
255248
|
-
await appendSteering2(
|
|
255503
|
+
await appendSteering2(join36(tasksDir, w2.changeName), message);
|
|
255249
255504
|
fileEmit({ type: "steering_submitted", changeName: w2.changeName, message });
|
|
255250
255505
|
} catch (err) {
|
|
255251
255506
|
const text = err.message;
|
|
@@ -255493,7 +255748,7 @@ var exports_list = {};
|
|
|
255493
255748
|
__export(exports_list, {
|
|
255494
255749
|
runList: () => runList
|
|
255495
255750
|
});
|
|
255496
|
-
import { join as
|
|
255751
|
+
import { join as join37 } from "path";
|
|
255497
255752
|
function countTaskItems(content) {
|
|
255498
255753
|
const checked = (content.match(/^- \[x\]/gm) ?? []).length;
|
|
255499
255754
|
const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
|
|
@@ -255506,13 +255761,13 @@ function buildLocalRows(statesDir, projectRoot) {
|
|
|
255506
255761
|
const sources = [{ dir: statesDir, label: "main" }];
|
|
255507
255762
|
const worktreesRoot = worktreesDir2(projectRoot);
|
|
255508
255763
|
for (const wt of storage.list(worktreesRoot)) {
|
|
255509
|
-
sources.push({ dir:
|
|
255764
|
+
sources.push({ dir: join37(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
|
|
255510
255765
|
}
|
|
255511
255766
|
for (const { dir, label } of sources) {
|
|
255512
255767
|
for (const entry of storage.list(dir)) {
|
|
255513
255768
|
if (seen.has(entry))
|
|
255514
255769
|
continue;
|
|
255515
|
-
const raw = storage.read(
|
|
255770
|
+
const raw = storage.read(join37(dir, entry, ".ralph-state.json"));
|
|
255516
255771
|
if (raw === null)
|
|
255517
255772
|
continue;
|
|
255518
255773
|
let state;
|
|
@@ -255527,7 +255782,7 @@ function buildLocalRows(statesDir, projectRoot) {
|
|
|
255527
255782
|
const firstLine = promptRaw.split(`
|
|
255528
255783
|
`).find((l3) => l3.trim() !== "") ?? "";
|
|
255529
255784
|
let progress = "\u2014";
|
|
255530
|
-
const tasksContent = storage.read(
|
|
255785
|
+
const tasksContent = storage.read(join37(dir, entry, "tasks.md"));
|
|
255531
255786
|
if (tasksContent !== null) {
|
|
255532
255787
|
const { checked, unchecked } = countTaskItems(tasksContent);
|
|
255533
255788
|
const total = checked + unchecked;
|
|
@@ -255939,7 +256194,7 @@ var exports_json_runner = {};
|
|
|
255939
256194
|
__export(exports_json_runner, {
|
|
255940
256195
|
runAgentJson: () => runAgentJson
|
|
255941
256196
|
});
|
|
255942
|
-
import { join as
|
|
256197
|
+
import { join as join38 } from "path";
|
|
255943
256198
|
import { mkdir as mkdir13 } from "fs/promises";
|
|
255944
256199
|
import { homedir as homedir6 } from "os";
|
|
255945
256200
|
function makeEmit(fileSink) {
|
|
@@ -255961,7 +256216,7 @@ async function runAgentJson({
|
|
|
255961
256216
|
tasksDir,
|
|
255962
256217
|
runPreflight: runPreflight2 = runPreflight
|
|
255963
256218
|
}) {
|
|
255964
|
-
await mkdir13(
|
|
256219
|
+
await mkdir13(join38(homedir6(), ".ralph"), { recursive: true }).catch(() => {
|
|
255965
256220
|
return;
|
|
255966
256221
|
});
|
|
255967
256222
|
const fileSink = createJsonLogFileSink(args.jsonLogFile);
|
|
@@ -256169,7 +256424,7 @@ __export(exports_src2, {
|
|
|
256169
256424
|
main: () => main2
|
|
256170
256425
|
});
|
|
256171
256426
|
import { mkdir as mkdir14 } from "fs/promises";
|
|
256172
|
-
import { join as
|
|
256427
|
+
import { join as join39 } from "path";
|
|
256173
256428
|
async function main2(argv) {
|
|
256174
256429
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
256175
256430
|
printAgentHelp();
|
|
@@ -256220,7 +256475,7 @@ async function main2(argv) {
|
|
|
256220
256475
|
}
|
|
256221
256476
|
await mkdir14(statesDir, { recursive: true });
|
|
256222
256477
|
await mkdir14(tasksDir, { recursive: true });
|
|
256223
|
-
await mkdir14(
|
|
256478
|
+
await mkdir14(join39(projectRoot, ".ralph"), { recursive: true });
|
|
256224
256479
|
if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
|
|
256225
256480
|
process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
|
|
256226
256481
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neriros/ralphy",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.13",
|
|
4
4
|
"description": "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/
|
|
18
|
+
"url": "https://github.com/rosneri/ralphy.git"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
21
|
"ralphy": "./dist/shell/index.js",
|
|
@@ -95,7 +95,8 @@
|
|
|
95
95
|
"@babel/plugin-transform-modules-systemjs": "^7.29.4",
|
|
96
96
|
"axios": "^1.15.1",
|
|
97
97
|
"fast-uri": "^3.1.2",
|
|
98
|
-
"minimatch": "^10.2.3"
|
|
98
|
+
"minimatch": "^10.2.3",
|
|
99
|
+
"tmp": "^0.2.6"
|
|
99
100
|
},
|
|
100
101
|
"engines": {
|
|
101
102
|
"bun": ">=1.0.0"
|