@neriros/ralphy 2.7.4 → 2.7.6
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 +18 -9
- package/dist/cli/index.js +90 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,11 +104,13 @@ Defaults are written to `ralphy.config.json` on first run; CLI flags override co
|
|
|
104
104
|
"doneStatus": "In Review",
|
|
105
105
|
"doneLabel": "ralphy-done",
|
|
106
106
|
"postComments": true,
|
|
107
|
+
"updateEveryIterations": 10,
|
|
107
108
|
},
|
|
108
109
|
"useWorktree": true,
|
|
109
110
|
"cleanupWorktreeOnSuccess": false,
|
|
110
111
|
"setupScript": "bun install",
|
|
111
112
|
"teardownScript": "git status",
|
|
113
|
+
"appendPrompt": "Always run lint before committing.",
|
|
112
114
|
}
|
|
113
115
|
```
|
|
114
116
|
|
|
@@ -120,6 +122,10 @@ With `--worktree` (or `useWorktree: true` in config) each task runs in an isolat
|
|
|
120
122
|
|
|
121
123
|
Use `setupScript` (run inside the worktree right after scaffolding) to install dependencies, copy `.env`, etc. Use `teardownScript` (run after the loop exits, before any worktree cleanup) to gather artifacts or roll back local mutations. Both run via `sh -c`; failures are logged but never block the loop. With `cleanupWorktreeOnSuccess: true` the worktree is removed when the worker exits 0 — failed workers always keep their worktree (and branch) for human inspection.
|
|
122
124
|
|
|
125
|
+
**`appendPrompt`** (or `--prompt` in agent mode) is appended to every scaffolded `proposal.md` under an `## Additional instructions` section — use it for cross-cutting guidance every task should see.
|
|
126
|
+
|
|
127
|
+
**`updateEveryIterations`** (default `10`, `0` disables) posts a "🔄 Ralph progress update: iteration N" comment on the Linear issue every N task iterations. Requires `postComments: true`.
|
|
128
|
+
|
|
123
129
|
Failed workers (non-zero exit) are not marked processed, so they'll be retried on the next poll. SIGINT/SIGTERM cleanly stops polling and kills active workers. All Linear side effects are best-effort — failures log a warning but never block the task loop.
|
|
124
130
|
|
|
125
131
|
## CLI Options
|
|
@@ -143,15 +149,18 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
|
|
|
143
149
|
|
|
144
150
|
### Agent mode flags
|
|
145
151
|
|
|
146
|
-
| Option
|
|
147
|
-
|
|
|
148
|
-
| `--linear-team <key>`
|
|
149
|
-
| `--linear-assignee <id>`
|
|
150
|
-
| `--linear-status <name>`
|
|
151
|
-
| `--linear-label <name>`
|
|
152
|
-
| `--poll-interval <s>`
|
|
153
|
-
| `--concurrency <n>`
|
|
154
|
-
| `--worktree`
|
|
152
|
+
| Option | Description |
|
|
153
|
+
| ----------------------------- | --------------------------------------------- |
|
|
154
|
+
| `--linear-team <key>` | Linear team key (e.g. `ENG`) |
|
|
155
|
+
| `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`) |
|
|
156
|
+
| `--linear-status <name>` | Filter by status name (repeatable) |
|
|
157
|
+
| `--linear-label <name>` | Filter by label name (repeatable, any-of) |
|
|
158
|
+
| `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
|
|
159
|
+
| `--concurrency <n>` | Max concurrent task loops (default: 1) |
|
|
160
|
+
| `--worktree` | Run each task in its own git worktree |
|
|
161
|
+
| `--in-progress-status <name>` | Linear status to set when work starts |
|
|
162
|
+
| `--done-status <name>` | Linear status to set on successful completion |
|
|
163
|
+
| `--done-label <name>` | Linear label to add on successful completion |
|
|
155
164
|
|
|
156
165
|
## OpenSpec Flow
|
|
157
166
|
|
package/dist/cli/index.js
CHANGED
|
@@ -56131,7 +56131,7 @@ var HELP_TEXT = [
|
|
|
56131
56131
|
"",
|
|
56132
56132
|
"Options:",
|
|
56133
56133
|
" --name <name> Change name (required for most commands)",
|
|
56134
|
-
" --prompt <text> Task description",
|
|
56134
|
+
" --prompt <text> Task description (in agent mode: appended to every scaffolded proposal)",
|
|
56135
56135
|
" --prompt-file <path> Read prompt from file",
|
|
56136
56136
|
" --model <model> Set model (haiku|sonnet|opus)",
|
|
56137
56137
|
" --claude [model] Use Claude engine (haiku|sonnet|opus, default: opus)",
|
|
@@ -56153,6 +56153,9 @@ var HELP_TEXT = [
|
|
|
56153
56153
|
" --poll-interval <s> Seconds between Linear polls (default: 60)",
|
|
56154
56154
|
" --concurrency <n> Max concurrent task loops (default: 1)",
|
|
56155
56155
|
" --worktree Run each task in its own git worktree (.ralph/worktrees/<name>)",
|
|
56156
|
+
" --in-progress-status <name> Linear status to set when work starts on an issue",
|
|
56157
|
+
" --done-status <name> Linear status to set when work completes successfully",
|
|
56158
|
+
" --done-label <name> Linear label to add when work completes successfully",
|
|
56156
56159
|
"",
|
|
56157
56160
|
" --help, -h Show this help message",
|
|
56158
56161
|
"",
|
|
@@ -56189,7 +56192,10 @@ async function parseArgs(argv) {
|
|
|
56189
56192
|
linearLabel: [],
|
|
56190
56193
|
pollInterval: 60,
|
|
56191
56194
|
concurrency: 1,
|
|
56192
|
-
worktree: false
|
|
56195
|
+
worktree: false,
|
|
56196
|
+
inProgressStatus: "",
|
|
56197
|
+
doneStatus: "",
|
|
56198
|
+
doneLabel: ""
|
|
56193
56199
|
};
|
|
56194
56200
|
let expectModel = false;
|
|
56195
56201
|
let expectModelFlag = false;
|
|
@@ -56209,6 +56215,9 @@ async function parseArgs(argv) {
|
|
|
56209
56215
|
let expectLinearLabel = false;
|
|
56210
56216
|
let expectPollInterval = false;
|
|
56211
56217
|
let expectConcurrency = false;
|
|
56218
|
+
let expectInProgressStatus = false;
|
|
56219
|
+
let expectDoneStatus = false;
|
|
56220
|
+
let expectDoneLabel = false;
|
|
56212
56221
|
for (const arg of argv) {
|
|
56213
56222
|
if (expectModel) {
|
|
56214
56223
|
if (VALID_MODELS.has(arg)) {
|
|
@@ -56304,6 +56313,21 @@ async function parseArgs(argv) {
|
|
|
56304
56313
|
expectConcurrency = false;
|
|
56305
56314
|
continue;
|
|
56306
56315
|
}
|
|
56316
|
+
if (expectInProgressStatus) {
|
|
56317
|
+
result2.inProgressStatus = arg;
|
|
56318
|
+
expectInProgressStatus = false;
|
|
56319
|
+
continue;
|
|
56320
|
+
}
|
|
56321
|
+
if (expectDoneStatus) {
|
|
56322
|
+
result2.doneStatus = arg;
|
|
56323
|
+
expectDoneStatus = false;
|
|
56324
|
+
continue;
|
|
56325
|
+
}
|
|
56326
|
+
if (expectDoneLabel) {
|
|
56327
|
+
result2.doneLabel = arg;
|
|
56328
|
+
expectDoneLabel = false;
|
|
56329
|
+
continue;
|
|
56330
|
+
}
|
|
56307
56331
|
switch (arg) {
|
|
56308
56332
|
case "--claude":
|
|
56309
56333
|
if (result2.engineSet && result2.engine !== "claude") {
|
|
@@ -56383,6 +56407,15 @@ async function parseArgs(argv) {
|
|
|
56383
56407
|
case "--worktree":
|
|
56384
56408
|
result2.worktree = true;
|
|
56385
56409
|
break;
|
|
56410
|
+
case "--in-progress-status":
|
|
56411
|
+
expectInProgressStatus = true;
|
|
56412
|
+
break;
|
|
56413
|
+
case "--done-status":
|
|
56414
|
+
expectDoneStatus = true;
|
|
56415
|
+
break;
|
|
56416
|
+
case "--done-label":
|
|
56417
|
+
expectDoneLabel = true;
|
|
56418
|
+
break;
|
|
56386
56419
|
default:
|
|
56387
56420
|
if (VALID_MODES.has(arg)) {
|
|
56388
56421
|
result2.mode = arg;
|
|
@@ -69739,7 +69772,7 @@ function changeNameForIssue(issue) {
|
|
|
69739
69772
|
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
69740
69773
|
return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
|
|
69741
69774
|
}
|
|
69742
|
-
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = []) {
|
|
69775
|
+
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
|
|
69743
69776
|
const name = changeNameForIssue(issue);
|
|
69744
69777
|
const changeDir = join11(tasksDir, name);
|
|
69745
69778
|
const stateDir = join11(statesDir, name);
|
|
@@ -69769,6 +69802,7 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [])
|
|
|
69769
69802
|
"",
|
|
69770
69803
|
issue.description?.trim() || "_No description provided in Linear._",
|
|
69771
69804
|
...commentsBlock,
|
|
69805
|
+
...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
|
|
69772
69806
|
"",
|
|
69773
69807
|
"## Steering",
|
|
69774
69808
|
"",
|
|
@@ -69810,6 +69844,7 @@ var RalphyConfigSchema = exports_external.object({
|
|
|
69810
69844
|
cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
|
|
69811
69845
|
setupScript: exports_external.string().optional(),
|
|
69812
69846
|
teardownScript: exports_external.string().optional(),
|
|
69847
|
+
appendPrompt: exports_external.string().optional(),
|
|
69813
69848
|
engine: exports_external.enum(["claude", "codex"]).default("claude"),
|
|
69814
69849
|
model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
|
|
69815
69850
|
linear: exports_external.object({
|
|
@@ -69820,7 +69855,8 @@ var RalphyConfigSchema = exports_external.object({
|
|
|
69820
69855
|
inProgressStatus: exports_external.string().optional(),
|
|
69821
69856
|
doneStatus: exports_external.string().optional(),
|
|
69822
69857
|
doneLabel: exports_external.string().optional(),
|
|
69823
|
-
postComments: exports_external.boolean().default(true)
|
|
69858
|
+
postComments: exports_external.boolean().default(true),
|
|
69859
|
+
updateEveryIterations: exports_external.number().int().nonnegative().default(10)
|
|
69824
69860
|
}).default({ statuses: [], labels: [], postComments: true })
|
|
69825
69861
|
}).default({
|
|
69826
69862
|
concurrency: 1,
|
|
@@ -69906,8 +69942,37 @@ class AgentCoordinator {
|
|
|
69906
69942
|
state.lastPollAt = new Date().toISOString();
|
|
69907
69943
|
await this.deps.saveState(state);
|
|
69908
69944
|
this.spawnNext();
|
|
69945
|
+
await this.reportProgress();
|
|
69909
69946
|
return { found: issues.length, added };
|
|
69910
69947
|
}
|
|
69948
|
+
async reportProgress() {
|
|
69949
|
+
const updater = this.deps.updater;
|
|
69950
|
+
const everyN = this.opts.commentEveryIterations ?? 0;
|
|
69951
|
+
if (everyN <= 0 || !updater || this.opts.postComments === false || !this.deps.getIterationCount) {
|
|
69952
|
+
return;
|
|
69953
|
+
}
|
|
69954
|
+
for (const w of this.workers) {
|
|
69955
|
+
let count;
|
|
69956
|
+
try {
|
|
69957
|
+
count = await this.deps.getIterationCount(w.changeName);
|
|
69958
|
+
} catch (err) {
|
|
69959
|
+
this.deps.onLog(`! iteration count read failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
|
|
69960
|
+
continue;
|
|
69961
|
+
}
|
|
69962
|
+
if (count < everyN)
|
|
69963
|
+
continue;
|
|
69964
|
+
const currMilestone = Math.floor(count / everyN);
|
|
69965
|
+
const lastMilestone = Math.floor(w.lastReportedIteration / everyN);
|
|
69966
|
+
if (currMilestone <= lastMilestone)
|
|
69967
|
+
continue;
|
|
69968
|
+
try {
|
|
69969
|
+
await updater.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
|
|
69970
|
+
w.lastReportedIteration = count;
|
|
69971
|
+
} catch (err) {
|
|
69972
|
+
this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
|
|
69973
|
+
}
|
|
69974
|
+
}
|
|
69975
|
+
}
|
|
69911
69976
|
spawnNext() {
|
|
69912
69977
|
if (this.stopped || !this.state)
|
|
69913
69978
|
return;
|
|
@@ -69937,7 +70002,9 @@ class AgentCoordinator {
|
|
|
69937
70002
|
changeName,
|
|
69938
70003
|
issueId: issue.id,
|
|
69939
70004
|
issueIdentifier: issue.identifier,
|
|
69940
|
-
|
|
70005
|
+
issue,
|
|
70006
|
+
kill: handle.kill,
|
|
70007
|
+
lastReportedIteration: 0
|
|
69941
70008
|
};
|
|
69942
70009
|
this.workers.push(worker);
|
|
69943
70010
|
this.pendingIds.delete(issue.id);
|
|
@@ -70124,6 +70191,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70124
70191
|
const teamKeyOf = (issue) => issue.identifier.split("-")[0];
|
|
70125
70192
|
const useWorktree = args.worktree || cfg.useWorktree;
|
|
70126
70193
|
const cwdByChange = new Map;
|
|
70194
|
+
const statesDirByChange = new Map;
|
|
70127
70195
|
async function runScript(label, cmd, cwd2) {
|
|
70128
70196
|
appendLog(` ${label}: ${cmd}`, "gray");
|
|
70129
70197
|
const proc = Bun.spawn({
|
|
@@ -70164,8 +70232,10 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70164
70232
|
appendLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
|
|
70165
70233
|
}
|
|
70166
70234
|
}
|
|
70167
|
-
const
|
|
70235
|
+
const appendPrompt = args.prompt || cfg.appendPrompt || "";
|
|
70236
|
+
const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
70168
70237
|
cwdByChange.set(changeName, workerCwd);
|
|
70238
|
+
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
70169
70239
|
if (cfg.setupScript) {
|
|
70170
70240
|
await runScript("setup", cfg.setupScript, workerCwd);
|
|
70171
70241
|
}
|
|
@@ -70213,6 +70283,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70213
70283
|
}
|
|
70214
70284
|
}
|
|
70215
70285
|
cwdByChange.delete(changeName);
|
|
70286
|
+
statesDirByChange.delete(changeName);
|
|
70216
70287
|
return code;
|
|
70217
70288
|
});
|
|
70218
70289
|
return { exited: wrapped, kill: () => proc.kill() };
|
|
@@ -70221,6 +70292,14 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70221
70292
|
saveState: (s) => writeAgentState(projectRoot, s),
|
|
70222
70293
|
onLog: appendLog,
|
|
70223
70294
|
onWorkersChanged: () => setTick((t) => t + 1),
|
|
70295
|
+
getIterationCount: async (changeName) => {
|
|
70296
|
+
const dir = statesDirByChange.get(changeName) ?? statesDir;
|
|
70297
|
+
const file = Bun.file(join14(dir, changeName, ".ralph-state.json"));
|
|
70298
|
+
if (!await file.exists())
|
|
70299
|
+
return 0;
|
|
70300
|
+
const json = await file.json();
|
|
70301
|
+
return json.iteration ?? 0;
|
|
70302
|
+
},
|
|
70224
70303
|
updater: {
|
|
70225
70304
|
postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
|
|
70226
70305
|
setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
|
|
@@ -70249,10 +70328,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70249
70328
|
}, {
|
|
70250
70329
|
concurrency,
|
|
70251
70330
|
filter: filter2,
|
|
70252
|
-
inProgressStatus: cfg.linear.inProgressStatus,
|
|
70253
|
-
doneStatus: cfg.linear.doneStatus,
|
|
70254
|
-
doneLabel: cfg.linear.doneLabel,
|
|
70255
|
-
postComments: cfg.linear.postComments
|
|
70331
|
+
inProgressStatus: args.inProgressStatus || cfg.linear.inProgressStatus,
|
|
70332
|
+
doneStatus: args.doneStatus || cfg.linear.doneStatus,
|
|
70333
|
+
doneLabel: args.doneLabel || cfg.linear.doneLabel,
|
|
70334
|
+
postComments: cfg.linear.postComments,
|
|
70335
|
+
commentEveryIterations: cfg.linear.updateEveryIterations
|
|
70256
70336
|
});
|
|
70257
70337
|
coordRef.current = coord2;
|
|
70258
70338
|
await coord2.init();
|