@neriros/ralphy 2.13.7 โ†’ 2.13.10

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 CHANGED
@@ -82,12 +82,15 @@ ralph status --name fix-auth # Detailed view of one task
82
82
  ```bash
83
83
  export LINEAR_API_KEY=lin_api_xxx
84
84
  ralph agent --linear-team ENG --linear-assignee me --concurrency 3 --poll-interval 60
85
+
86
+ # Limit the number of tickets processed in this run
87
+ ralph agent --max-tickets 5 --linear-team ENG --linear-assignee me
85
88
  ```
86
89
 
87
90
  What it does on each tick:
88
91
 
89
92
  1. Polls Linear for open issues matching the filter (team / assignee / status / labels)
90
- 2. Dedupes against `.ralph/agent-state.json` (already processed) plus any in-flight workers
93
+ 2. Dedupes against in-flight workers and any already-active issues
91
94
  3. For each new issue: fetches existing comments, scaffolds `openspec/changes/<id-slug>/{proposal.md,tasks.md,design.md}` (with the comments embedded so the worker sees prior discussion), then spawns `ralph task --name <id-slug>` up to the concurrency cap
92
95
  4. Posts a "๐Ÿค– started" comment on the Linear issue and applies the `setInProgress` indicator (if configured)
93
96
  5. On worker exit, posts a success/failure comment and applies the `setDone` indicator on success or `setError` on failure (if configured)
@@ -196,11 +199,41 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
196
199
  | `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`) |
197
200
  | `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
198
201
  | `--concurrency <n>` | Max concurrent task loops (default: 1) |
202
+ | `--max-tickets <n>` | Stop picking up new issues after N have been started this run (0 = unlimited) |
199
203
  | `--worktree` | Run each task in its own git worktree |
200
204
  | `--indicator <k>:<t>:<v>` | Override a `linear.indicators` entry; repeatable (e.g. `setDone:status:Done`) |
201
205
  | `--create-pr` | Push worker branch + open a GitHub PR on success (needs `--worktree`) |
202
206
  | `--fix-ci` | After PR opens, re-run task on CI failures until green (needs `--create-pr`) |
203
207
 
208
+ #### `--max-tickets`
209
+
210
+ Use `--max-tickets N` to cap how many issues ralph picks up in a single agent run. Once N issues have been started (across fresh, resume, and conflict-fix modes), the coordinator stops enqueuing new work. In-flight workers continue to completion. The limit resets each time you restart `ralph agent`.
211
+
212
+ ```bash
213
+ # Process at most 3 issues this session, then idle
214
+ ralph agent --max-tickets 3 --linear-team ENG
215
+ ```
216
+
217
+ When the limit is reached, ralph logs a yellow notice and the dashboard header shows `โ”‚ tickets โ‰คN`. Polling continues (to handle conflict re-fixes on already-started issues), but no new issues are queued.
218
+
219
+ #### Dashboard
220
+
221
+ The `ralph agent` terminal dashboard shows a full-terminal layout with three always-visible panels:
222
+
223
+ - **RALPH AGENT** (blue box): engine/model, concurrency, poll interval, active limits (`iter โ‰คN`, `cost โ‰ค$N`, `tickets โ‰คN`), feature flags (โ— PR โ— fixCI โ— worktree), and the Linear filter on the second line
224
+ - **POLL STATUS + WORKERS** (side-by-side): last-poll counts and next-poll countdown; active/queued worker totals with colored counts
225
+ - **TASKS tab bar** (when multiple workers run): numbered worker tabs with priority glyph and phase โ€” Tab/โ† โ†’ to switch, 1-9 to jump
226
+
227
+ Each worker card shows:
228
+
229
+ - Priority badge (`โ–ฒ URGENT` / `โ†‘ HIGH` / `ยท MED` / `โ†“ LOW`) + issue identifier + title + mode badge (`[NEW]` / `[RESUME]` / `[FIX]`)
230
+ - `โ†— LINEAR ISSUE-ID` and `โ†— PR #N` (short labels, not full URLs)
231
+ - `โ–ถ TASK` โ€” first unchecked task from `tasks.md`, updated every second
232
+ - `PHASE` with color (cyan = working, yellow = git ops, blue = CI, green = done, red = gave-up) + time in phase
233
+ - `โต CMD` when a shell command is in flight (shows the command and how long it's been running)
234
+ - `LOG` โ€” path to the worker's log file for `tail -f`
235
+ - `โ”€ OUTPUT โ”€` section with live stdout/stderr (scales to fill remaining terminal height for the focused worker)
236
+
204
237
  ## OpenSpec Flow
205
238
 
206
239
  There are no phases. One loop, one prompt, one `tasks.md` checklist.
package/dist/cli/index.js CHANGED
@@ -48157,10 +48157,10 @@ var require_axios = __commonJS((exports, module) => {
48157
48157
  this.__CANCEL__ = true;
48158
48158
  }
48159
48159
  }
48160
- function settle(resolve, reject2, response) {
48160
+ function settle(resolve2, reject2, response) {
48161
48161
  const validateStatus = response.config.validateStatus;
48162
48162
  if (!response.status || !validateStatus || validateStatus(response.status)) {
48163
- resolve(response);
48163
+ resolve2(response);
48164
48164
  } else {
48165
48165
  reject2(new AxiosError("Request failed with status code " + response.status, response.status >= 400 && response.status < 500 ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE, response.config, response.request, response));
48166
48166
  }
@@ -49013,7 +49013,7 @@ var require_axios = __commonJS((exports, module) => {
49013
49013
  }
49014
49014
  var isHttpAdapterSupported = typeof process !== "undefined" && utils$1.kindOf(process) === "process";
49015
49015
  var wrapAsync = (asyncExecutor) => {
49016
- return new Promise((resolve, reject2) => {
49016
+ return new Promise((resolve2, reject2) => {
49017
49017
  let onDone;
49018
49018
  let isDone;
49019
49019
  const done = (value, isRejected) => {
@@ -49024,7 +49024,7 @@ var require_axios = __commonJS((exports, module) => {
49024
49024
  };
49025
49025
  const _resolve = (value) => {
49026
49026
  done(value);
49027
- resolve(value);
49027
+ resolve2(value);
49028
49028
  };
49029
49029
  const _reject = (reason) => {
49030
49030
  done(reason, true);
@@ -49085,7 +49085,7 @@ var require_axios = __commonJS((exports, module) => {
49085
49085
  }
49086
49086
  };
49087
49087
  var httpAdapter = isHttpAdapterSupported && function httpAdapter2(config) {
49088
- return wrapAsync(async function dispatchHttpRequest(resolve, reject2, onDone) {
49088
+ return wrapAsync(async function dispatchHttpRequest(resolve2, reject2, onDone) {
49089
49089
  const own2 = (key) => utils$1.hasOwnProp(config, key) ? config[key] : undefined;
49090
49090
  let data = own2("data");
49091
49091
  let lookup = own2("lookup");
@@ -49193,7 +49193,7 @@ var require_axios = __commonJS((exports, module) => {
49193
49193
  }
49194
49194
  let convertedData;
49195
49195
  if (method2 !== "GET") {
49196
- return settle(resolve, reject2, {
49196
+ return settle(resolve2, reject2, {
49197
49197
  status: 405,
49198
49198
  statusText: "method not allowed",
49199
49199
  headers: {},
@@ -49215,7 +49215,7 @@ var require_axios = __commonJS((exports, module) => {
49215
49215
  } else if (responseType === "stream") {
49216
49216
  convertedData = stream.Readable.from(convertedData);
49217
49217
  }
49218
- return settle(resolve, reject2, {
49218
+ return settle(resolve2, reject2, {
49219
49219
  data: convertedData,
49220
49220
  status: 200,
49221
49221
  statusText: "OK",
@@ -49442,7 +49442,7 @@ var require_axios = __commonJS((exports, module) => {
49442
49442
  });
49443
49443
  }
49444
49444
  response.data = responseStream;
49445
- settle(resolve, reject2, response);
49445
+ settle(resolve2, reject2, response);
49446
49446
  } else {
49447
49447
  const responseBuffer = [];
49448
49448
  let totalResponseBytes = 0;
@@ -49481,7 +49481,7 @@ var require_axios = __commonJS((exports, module) => {
49481
49481
  } catch (err) {
49482
49482
  return reject2(AxiosError.from(err, null, config, response.request, response));
49483
49483
  }
49484
- settle(resolve, reject2, response);
49484
+ settle(resolve2, reject2, response);
49485
49485
  });
49486
49486
  }
49487
49487
  abortEmitter.once("abort", (err) => {
@@ -49782,7 +49782,7 @@ var require_axios = __commonJS((exports, module) => {
49782
49782
  };
49783
49783
  var isXHRAdapterSupported = typeof XMLHttpRequest !== "undefined";
49784
49784
  var xhrAdapter = isXHRAdapterSupported && function(config) {
49785
- return new Promise(function dispatchXhrRequest(resolve, reject2) {
49785
+ return new Promise(function dispatchXhrRequest(resolve2, reject2) {
49786
49786
  const _config = resolveConfig(config);
49787
49787
  let requestData = _config.data;
49788
49788
  const requestHeaders = AxiosHeaders.from(_config.headers).normalize();
@@ -49818,7 +49818,7 @@ var require_axios = __commonJS((exports, module) => {
49818
49818
  request
49819
49819
  };
49820
49820
  settle(function _resolve(value) {
49821
- resolve(value);
49821
+ resolve2(value);
49822
49822
  done();
49823
49823
  }, function _reject(err) {
49824
49824
  reject2(err);
@@ -50247,8 +50247,8 @@ var require_axios = __commonJS((exports, module) => {
50247
50247
  }
50248
50248
  }
50249
50249
  !isStreamResponse && unsubscribe && unsubscribe();
50250
- return await new Promise((resolve, reject2) => {
50251
- settle(resolve, reject2, {
50250
+ return await new Promise((resolve2, reject2) => {
50251
+ settle(resolve2, reject2, {
50252
50252
  data: responseData,
50253
50253
  headers: AxiosHeaders.from(response.headers),
50254
50254
  status: response.status,
@@ -50640,8 +50640,8 @@ var require_axios = __commonJS((exports, module) => {
50640
50640
  throw new TypeError("executor must be a function.");
50641
50641
  }
50642
50642
  let resolvePromise;
50643
- this.promise = new Promise(function promiseExecutor(resolve) {
50644
- resolvePromise = resolve;
50643
+ this.promise = new Promise(function promiseExecutor(resolve2) {
50644
+ resolvePromise = resolve2;
50645
50645
  });
50646
50646
  const token = this;
50647
50647
  this.promise.then((cancel) => {
@@ -50655,9 +50655,9 @@ var require_axios = __commonJS((exports, module) => {
50655
50655
  });
50656
50656
  this.promise.then = (onfulfilled) => {
50657
50657
  let _resolve;
50658
- const promise = new Promise((resolve) => {
50659
- token.subscribe(resolve);
50660
- _resolve = resolve;
50658
+ const promise = new Promise((resolve2) => {
50659
+ token.subscribe(resolve2);
50660
+ _resolve = resolve2;
50661
50661
  }).then(onfulfilled);
50662
50662
  promise.cancel = function reject2() {
50663
50663
  token.unsubscribe(_resolve);
@@ -50837,7 +50837,7 @@ var require_axios = __commonJS((exports, module) => {
50837
50837
  });
50838
50838
 
50839
50839
  // apps/cli/src/index.ts
50840
- import { resolve, join as join19, dirname as dirname5 } from "path";
50840
+ import { resolve as resolve2, join as join19, dirname as dirname5 } from "path";
50841
50841
  import { exists as exists2, mkdir as mkdir5, rm } from "fs/promises";
50842
50842
 
50843
50843
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
@@ -56406,102 +56406,36 @@ function styled(text, style) {
56406
56406
  function log(msg) {
56407
56407
  console.log(typeof msg === "string" ? msg : format(msg));
56408
56408
  }
56409
- // package.json
56410
- var package_default = {
56411
- name: "@neriros/ralphy",
56412
- version: "2.13.7",
56413
- description: "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
56414
- keywords: [
56415
- "agent",
56416
- "ai",
56417
- "claude",
56418
- "cli",
56419
- "loop",
56420
- "mcp",
56421
- "ralph",
56422
- "task-runner"
56423
- ],
56424
- license: "MIT",
56425
- repository: {
56426
- type: "git",
56427
- url: "https://github.com/NeriRos/ralphy.git"
56428
- },
56429
- bin: {
56430
- ralphy: "./dist/cli/index.js",
56431
- "ralphy-mcp": "./dist/mcp/index.js"
56432
- },
56433
- workspaces: [
56434
- "packages/*",
56435
- "apps/*",
56436
- "tools/generators/*"
56437
- ],
56438
- files: [
56439
- "dist/cli/index.js",
56440
- "dist/mcp/index.js",
56441
- "README.md"
56442
- ],
56443
- type: "module",
56444
- scripts: {
56445
- ralph: "bun .ralph/bin/cli.js",
56446
- lint: "oxlint --config .oxlintrc.json .",
56447
- "lint:fix": "oxlint --config .oxlintrc.json --fix .",
56448
- fmt: "oxfmt --write .",
56449
- "fmt:check": "oxfmt --check .",
56450
- typecheck: "nx affected -t typecheck",
56451
- "check:deps": "depcruise packages/*/src apps/*/src --config .dependency-cruiser.cjs",
56452
- "check:unused": "knip",
56453
- "lint:ci": "nx affected -t lint --exclude=ui",
56454
- "fmt:ci": "nx affected -t fmt:check --exclude=ui",
56455
- "typecheck:ci": "nx affected -t typecheck --parallel=1 --exclude=ui",
56456
- "test:ci": "nx affected -t test --exclude=ui",
56457
- "test:affected-files:coverage:ci": "bun scripts/bun-test-affected-files.ts --coverage",
56458
- "test:coverage:ci": "nx affected -t test:coverage --exclude=ui",
56459
- "build:ci": "nx affected -t build --exclude=ui",
56460
- "check:circular:ci": "depcruise packages/*/src apps/*/src --config .dependency-cruiser.cjs",
56461
- "check:unused:ci": "knip",
56462
- "check:outdated:ci": "bun scripts/check-outdated.ts",
56463
- prepare: "bunx husky",
56464
- "build:publish": "bunx nx run-many --target=build --projects=cli,mcp --output-style stream && bun run copy-assets",
56465
- "copy-assets": "bun scripts/copy-assets.ts",
56466
- prepublishOnly: "bun run build:publish"
56467
- },
56468
- devDependencies: {
56469
- "@commitlint/cli": "^20.4.3",
56470
- "@commitlint/config-conventional": "^20.4.3",
56471
- "@fission-ai/openspec": "latest",
56472
- "@modelcontextprotocol/sdk": "^1.12.0",
56473
- "@nx/devkit": "^22.7.1",
56474
- "@nx/js": "^22.7.1",
56475
- "@secretlint/secretlint-rule-preset-recommend": "^11.3.1",
56476
- "@swc-node/register": "^1.11.1",
56477
- "@swc/core": "^1.15.18",
56478
- "@total-typescript/ts-reset": "^0.6.1",
56479
- "@types/node": "^22.0.0",
56480
- "bun-types": "^1.3.0",
56481
- chalk: "^5.4.0",
56482
- "dependency-cruiser": "^17.3.8",
56483
- husky: "^9.1.7",
56484
- knip: "^5.85.0",
56485
- "lint-staged": "^16.3.2",
56486
- nx: "22.5.3",
56487
- "oxc-parser": "^0.126.0",
56488
- oxfmt: "^0.36.0",
56489
- oxlint: "^1.51.0",
56490
- secretlint: "^11.3.1",
56491
- typescript: "^5.8.0",
56492
- zod: "^3.24.0"
56493
- },
56494
- overrides: {
56495
- axios: "^1.15.1",
56496
- minimatch: "^10.2.3"
56497
- },
56498
- engines: {
56499
- bun: ">=1.0.0"
56500
- }
56501
- };
56502
56409
 
56503
56410
  // apps/cli/src/cli.ts
56504
- var VERSION = package_default.version;
56411
+ import { readFileSync as readFileSync2 } from "fs";
56412
+ import { resolve } from "path";
56413
+ function getVersion() {
56414
+ const dirsToTry = [];
56415
+ try {
56416
+ const cliDir = import.meta.dir;
56417
+ dirsToTry.push(cliDir);
56418
+ } catch {}
56419
+ dirsToTry.push(process.cwd());
56420
+ for (const startDir of dirsToTry) {
56421
+ let current = startDir;
56422
+ for (let i = 0;i < 10; i++) {
56423
+ const pkgPath = resolve(current, "package.json");
56424
+ try {
56425
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
56426
+ if (pkg.workspaces && pkg.version && pkg.version !== "0.0.0") {
56427
+ return pkg.version;
56428
+ }
56429
+ } catch {}
56430
+ const parent = resolve(current, "..");
56431
+ if (parent === current)
56432
+ break;
56433
+ current = parent;
56434
+ }
56435
+ }
56436
+ return "unknown";
56437
+ }
56438
+ var VERSION = getVersion();
56505
56439
  var VALID_MODES = new Set(["task", "list", "status", "init", "agent", "clean"]);
56506
56440
  var VALID_MODELS = new Set(["haiku", "sonnet", "opus"]);
56507
56441
  var INDICATOR_KEYS = new Set([
@@ -56541,6 +56475,7 @@ var HELP_TEXT = [
56541
56475
  " --max-runtime <n> Stop after N minutes of wall-clock time (0 = no limit)",
56542
56476
  " --max-failures <n> Stop after N consecutive failures (default: 5, 0 = disable)",
56543
56477
  " --unlimited No iteration limit (default)",
56478
+ " --manual-test Enable manual testing phase (create test tasks in tasks.md)",
56544
56479
  " --log Log raw engine stream",
56545
56480
  " --verbose Verbose output",
56546
56481
  "",
@@ -56559,6 +56494,7 @@ var HELP_TEXT = [
56559
56494
  " Types: label, status",
56560
56495
  " --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
56561
56496
  " --fix-ci After opening the PR, re-run on CI failures until green (needs --create-pr)",
56497
+ " --max-tickets <n> Stop picking up new issues after N have been started (0 = unlimited)",
56562
56498
  "",
56563
56499
  " --help, -h Show this help message",
56564
56500
  "",
@@ -56636,6 +56572,7 @@ async function parseArgs(argv) {
56636
56572
  delay: 0,
56637
56573
  log: false,
56638
56574
  verbose: false,
56575
+ manualTest: false,
56639
56576
  linearTeam: "",
56640
56577
  linearAssignee: "",
56641
56578
  pollInterval: 60,
@@ -56643,7 +56580,8 @@ async function parseArgs(argv) {
56643
56580
  worktree: false,
56644
56581
  indicators: {},
56645
56582
  createPr: false,
56646
- fixCi: false
56583
+ fixCi: false,
56584
+ maxTickets: 0
56647
56585
  };
56648
56586
  let expectModel = false;
56649
56587
  let expectModelFlag = false;
@@ -56659,6 +56597,7 @@ async function parseArgs(argv) {
56659
56597
  let expectLinearAssignee = false;
56660
56598
  let expectPollInterval = false;
56661
56599
  let expectConcurrency = false;
56600
+ let expectMaxTickets = false;
56662
56601
  let expectIndicator = false;
56663
56602
  for (const arg of argv) {
56664
56603
  if (expectModel) {
@@ -56737,6 +56676,11 @@ async function parseArgs(argv) {
56737
56676
  expectConcurrency = false;
56738
56677
  continue;
56739
56678
  }
56679
+ if (expectMaxTickets) {
56680
+ result2.maxTickets = parseInt(arg, 10);
56681
+ expectMaxTickets = false;
56682
+ continue;
56683
+ }
56740
56684
  if (expectIndicator) {
56741
56685
  const { key, marker } = parseIndicatorArg(arg);
56742
56686
  mergeIndicator(result2.indicators, key, marker);
@@ -56807,6 +56751,9 @@ async function parseArgs(argv) {
56807
56751
  case "--concurrency":
56808
56752
  expectConcurrency = true;
56809
56753
  break;
56754
+ case "--max-tickets":
56755
+ expectMaxTickets = true;
56756
+ break;
56810
56757
  case "--worktree":
56811
56758
  result2.worktree = true;
56812
56759
  break;
@@ -56819,6 +56766,9 @@ async function parseArgs(argv) {
56819
56766
  case "--fix-ci":
56820
56767
  result2.fixCi = true;
56821
56768
  break;
56769
+ case "--manual-test":
56770
+ result2.manualTest = true;
56771
+ break;
56822
56772
  default:
56823
56773
  if (VALID_MODES.has(arg)) {
56824
56774
  result2.mode = arg;
@@ -56834,7 +56784,7 @@ async function parseArgs(argv) {
56834
56784
  // packages/context/src/context.ts
56835
56785
  import { AsyncLocalStorage } from "async_hooks";
56836
56786
  import {
56837
- readFileSync as readFileSync2,
56787
+ readFileSync as readFileSync3,
56838
56788
  writeFileSync,
56839
56789
  existsSync as existsSync2,
56840
56790
  unlinkSync,
@@ -56847,7 +56797,7 @@ class FileSystemProvider {
56847
56797
  read(path) {
56848
56798
  if (!existsSync2(path))
56849
56799
  return null;
56850
- return readFileSync2(path, "utf-8");
56800
+ return readFileSync3(path, "utf-8");
56851
56801
  }
56852
56802
  write(path, content) {
56853
56803
  mkdirSync(dirname(path), { recursive: true });
@@ -60907,6 +60857,7 @@ var StateSchema = exports_external.object({
60907
60857
  lastModified: exports_external.string(),
60908
60858
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
60909
60859
  model: exports_external.string().default("opus"),
60860
+ manualTest: exports_external.boolean().default(false),
60910
60861
  usage: UsageSchema.default({}),
60911
60862
  history: exports_external.array(HistoryEntrySchema).default([]),
60912
60863
  metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
@@ -60981,6 +60932,7 @@ function buildInitialState(options) {
60981
60932
  prompt: options.prompt,
60982
60933
  engine: options.engine ?? "claude",
60983
60934
  model: options.model ?? "opus",
60935
+ manualTest: options.manualTest ?? false,
60984
60936
  createdAt: now2,
60985
60937
  lastModified: now2,
60986
60938
  metadata: { branch }
@@ -65738,10 +65690,10 @@ async function runEngine(opts) {
65738
65690
  rawWriter.write(line + `
65739
65691
  `);
65740
65692
  };
65741
- const closeRaw = () => new Promise((resolve) => {
65693
+ const closeRaw = () => new Promise((resolve2) => {
65742
65694
  if (!rawWriter)
65743
- return resolve();
65744
- rawWriter.end(resolve);
65695
+ return resolve2();
65696
+ rawWriter.end(resolve2);
65745
65697
  });
65746
65698
  const emit = opts.onFeedEvent;
65747
65699
  function emitEvent(event) {
@@ -66567,14 +66519,14 @@ async function addSourceContext(frames) {
66567
66519
  return frames;
66568
66520
  }
66569
66521
  function getContextLinesFromFile(path, ranges, output) {
66570
- return new Promise((resolve) => {
66522
+ return new Promise((resolve2) => {
66571
66523
  const stream = createReadStream(path);
66572
66524
  const lineReaded = createInterface({
66573
66525
  input: stream
66574
66526
  });
66575
66527
  function destroyStreamAndResolve() {
66576
66528
  stream.destroy();
66577
- resolve();
66529
+ resolve2();
66578
66530
  }
66579
66531
  let lineNumber = 0;
66580
66532
  let currentRangeIndex = 0;
@@ -69106,15 +69058,15 @@ class PostHogBackendClient extends PostHogCoreStateless {
69106
69058
  if (this.featureFlagsPoller === undefined) {
69107
69059
  return false;
69108
69060
  }
69109
- return new Promise((resolve) => {
69061
+ return new Promise((resolve2) => {
69110
69062
  const timeout = setTimeout(() => {
69111
69063
  cleanup();
69112
- resolve(false);
69064
+ resolve2(false);
69113
69065
  }, timeoutMs);
69114
69066
  const cleanup = this._events.on("localEvaluationFlagsLoaded", (count) => {
69115
69067
  clearTimeout(timeout);
69116
69068
  cleanup();
69117
- resolve(count > 0);
69069
+ resolve2(count > 0);
69118
69070
  });
69119
69071
  });
69120
69072
  }
@@ -69601,6 +69553,34 @@ function buildTaskPrompt(state, taskDir) {
69601
69553
 
69602
69554
  `;
69603
69555
  }
69556
+ if (state.manualTest) {
69557
+ const tasksContent2 = storage.read(join7(taskDir, "tasks.md"));
69558
+ const hasUncheckedTasks = tasksContent2 !== null && /^- \[ \]/m.test(tasksContent2);
69559
+ if (!hasUncheckedTasks) {
69560
+ const hasManualTestSection = tasksContent2 !== null && /^## Manual Testing/m.test(tasksContent2);
69561
+ if (!hasManualTestSection) {
69562
+ prompt += `---
69563
+
69564
+ ## Manual Testing Phase
69565
+
69566
+ `;
69567
+ prompt += `All primary implementation tasks are complete. Now create manual test tasks.
69568
+
69569
+ `;
69570
+ prompt += `1. Analyze the specification and implementation
69571
+ `;
69572
+ prompt += `2. Identify critical manual test scenarios (UI interactions, edge cases, user workflows, integration testing)
69573
+ `;
69574
+ prompt += "3. Add a `## Manual Testing` section to tasks.md with test items as `- [ ] Test scenario description`\n";
69575
+ prompt += `4. Complete each test and check it off when done
69576
+
69577
+ `;
69578
+ prompt += `---
69579
+
69580
+ `;
69581
+ }
69582
+ }
69583
+ }
69604
69584
  prompt += `Change name: \`${state.name}\`
69605
69585
 
69606
69586
  `;
@@ -69718,7 +69698,7 @@ function mergeUsage(base2, resumed) {
69718
69698
  }
69719
69699
  // apps/cli/src/hooks/useLoop.ts
69720
69700
  function sleep(seconds) {
69721
- return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
69701
+ return new Promise((resolve2) => setTimeout(resolve2, seconds * 1000));
69722
69702
  }
69723
69703
  function useLoop(opts) {
69724
69704
  const [state, setState] = import_react55.useState(null);
@@ -69772,7 +69752,8 @@ function useLoop(opts) {
69772
69752
  name: opts.name,
69773
69753
  prompt: opts.prompt,
69774
69754
  engine: opts.engine,
69775
- model: opts.model
69755
+ model: opts.model,
69756
+ manualTest: opts.manualTest
69776
69757
  });
69777
69758
  writeState(stateDir, currentState);
69778
69759
  }
@@ -70177,6 +70158,7 @@ var RalphyConfigSchema = exports_external.object({
70177
70158
  iterationDelaySeconds: exports_external.number().int().nonnegative().default(0),
70178
70159
  logRawStream: exports_external.boolean().default(false),
70179
70160
  taskVerbose: exports_external.boolean().default(false),
70161
+ enableManualTest: exports_external.boolean().default(false),
70180
70162
  useWorktree: exports_external.boolean().default(false),
70181
70163
  cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
70182
70164
  setupScript: exports_external.string().optional(),
@@ -70201,6 +70183,7 @@ var RalphyConfigSchema = exports_external.object({
70201
70183
  pollIntervalSeconds: 60,
70202
70184
  maxIterationsPerTask: 0,
70203
70185
  maxCostUsdPerTask: 0,
70186
+ enableManualTest: false,
70204
70187
  engine: "claude",
70205
70188
  model: "opus",
70206
70189
  linear: { postComments: true, updateEveryIterations: 10, indicators: {} }
@@ -70401,10 +70384,8 @@ function buildIssueFilter(spec) {
70401
70384
  branches.push({ state: { name: { in: statuses } } });
70402
70385
  if (labels.length > 0)
70403
70386
  branches.push({ labels: { some: { name: { in: labels } } } });
70404
- if (branches.length === 1)
70405
- Object.assign(where, branches[0]);
70406
- else
70407
- where.or = branches;
70387
+ for (const b of branches)
70388
+ Object.assign(where, b);
70408
70389
  } else {
70409
70390
  where.state = { type: { in: ["unstarted", "started", "backlog"] } };
70410
70391
  }
@@ -70562,6 +70543,7 @@ class AgentCoordinator {
70562
70543
  queue = [];
70563
70544
  stopped = false;
70564
70545
  conflictNotified = new Set;
70546
+ ticketsStarted = 0;
70565
70547
  constructor(deps, opts) {
70566
70548
  this.deps = deps;
70567
70549
  this.opts = opts;
@@ -70575,6 +70557,9 @@ class AgentCoordinator {
70575
70557
  get activeWorkers() {
70576
70558
  return this.workers;
70577
70559
  }
70560
+ get ticketsStartedCount() {
70561
+ return this.ticketsStarted;
70562
+ }
70578
70563
  async init() {}
70579
70564
  async pollOnce() {
70580
70565
  if (this.stopped)
@@ -70596,8 +70581,17 @@ class AgentCoordinator {
70596
70581
  const queuedIds = new Set(this.queue.map((q) => q.issue.id));
70597
70582
  const activeIds = new Set(this.workers.map((w) => w.issueId));
70598
70583
  const eligible = (id) => !queuedIds.has(id) && !activeIds.has(id) && !this.pendingIds.has(id);
70584
+ const maxT = this.opts.maxTickets ?? 0;
70585
+ const atTicketLimit = () => {
70586
+ if (maxT === 0)
70587
+ return false;
70588
+ const inFlight = this.ticketsStarted + this.queue.length + this.workers.length + this.pendingIds.size;
70589
+ return inFlight >= maxT;
70590
+ };
70599
70591
  let added = 0;
70600
70592
  for (const issue of inProgress) {
70593
+ if (atTicketLimit())
70594
+ break;
70601
70595
  if (!eligible(issue.id))
70602
70596
  continue;
70603
70597
  if (!this.dependenciesResolved(issue))
@@ -70607,6 +70601,8 @@ class AgentCoordinator {
70607
70601
  added += 1;
70608
70602
  }
70609
70603
  for (const issue of conflicted) {
70604
+ if (atTicketLimit())
70605
+ break;
70610
70606
  if (!eligible(issue.id))
70611
70607
  continue;
70612
70608
  this.queue.push({ issue, mode: "conflict-fix" });
@@ -70614,6 +70610,8 @@ class AgentCoordinator {
70614
70610
  added += 1;
70615
70611
  }
70616
70612
  for (const issue of todo) {
70613
+ if (atTicketLimit())
70614
+ break;
70617
70615
  if (!eligible(issue.id))
70618
70616
  continue;
70619
70617
  if (!this.dependenciesResolved(issue))
@@ -70808,6 +70806,11 @@ class AgentCoordinator {
70808
70806
  };
70809
70807
  this.workers.push(worker);
70810
70808
  this.pendingIds.delete(issue.id);
70809
+ this.ticketsStarted += 1;
70810
+ const maxT = this.opts.maxTickets ?? 0;
70811
+ if (maxT > 0 && this.ticketsStarted >= maxT) {
70812
+ this.deps.onLog(` ticket limit reached (${maxT}) \u2014 no new issues will be picked up`, "yellow");
70813
+ }
70811
70814
  capture("agent_worker_spawned", {
70812
70815
  spawn_mode: mode,
70813
70816
  issue_identifier: issue.identifier
@@ -71829,6 +71832,8 @@ PR: ${prUrl}` : ""
71829
71832
  c.push("--log");
71830
71833
  if (args.verbose || cfg.taskVerbose)
71831
71834
  c.push("--verbose");
71835
+ if (args.manualTest || cfg.enableManualTest)
71836
+ c.push("--manual-test");
71832
71837
  return c;
71833
71838
  }
71834
71839
  function defaultSpawn(changeName, cmd, cwd2, note) {
@@ -72054,7 +72059,8 @@ PR: ${prUrl}` : ""
72054
72059
  ...indicators.setConflicted !== undefined ? { setConflicted: indicators.setConflicted } : {},
72055
72060
  ...indicators.clearConflicted !== undefined ? { clearConflicted: indicators.clearConflicted } : {},
72056
72061
  postComments: cfg.linear.postComments,
72057
- commentEveryIterations: cfg.linear.updateEveryIterations
72062
+ commentEveryIterations: cfg.linear.updateEveryIterations,
72063
+ ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {}
72058
72064
  });
72059
72065
  const filterDesc = describeIndicators(indicators, team, assignee);
72060
72066
  return {
@@ -72496,6 +72502,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72496
72502
  cfg.maxCostUsdPerTask
72497
72503
  ]
72498
72504
  }, undefined, true, undefined, this),
72505
+ args.maxTickets > 0 && /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
72506
+ color: "yellow",
72507
+ children: [
72508
+ " \u2502 tickets \u2264",
72509
+ args.maxTickets
72510
+ ]
72511
+ }, undefined, true, undefined, this),
72499
72512
  cfg.createPrOnSuccess && /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
72500
72513
  color: "green",
72501
72514
  children: " \u25CF PR"
@@ -73152,6 +73165,41 @@ function ErrorMessage({ message }) {
73152
73165
  children: message
73153
73166
  }, undefined, false, undefined, this);
73154
73167
  }
73168
+ function TaskModeWrapper({ args, statesDir, tasksDir, projectRoot }) {
73169
+ const [config, setConfig] = import_react58.useState(null);
73170
+ const [error, setError] = import_react58.useState(null);
73171
+ import_react58.useEffect(() => {
73172
+ loadRalphyConfig(projectRoot).then((cfg) => setConfig({ manualTest: cfg.enableManualTest })).catch((err) => setError(err.message));
73173
+ }, [projectRoot]);
73174
+ if (error) {
73175
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
73176
+ message: `Error loading config: ${error}`
73177
+ }, undefined, false, undefined, this);
73178
+ }
73179
+ if (!config) {
73180
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {}, undefined, false, undefined, this);
73181
+ }
73182
+ const manualTest = args.manualTest || config.manualTest;
73183
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskLoop, {
73184
+ opts: {
73185
+ name: args.name,
73186
+ prompt: args.prompt,
73187
+ engine: args.engine,
73188
+ model: args.model,
73189
+ maxIterations: args.maxIterations,
73190
+ maxCostUsd: args.maxCostUsd,
73191
+ maxRuntimeMinutes: args.maxRuntimeMinutes,
73192
+ maxConsecutiveFailures: args.maxConsecutiveFailures,
73193
+ delay: args.delay,
73194
+ log: args.log,
73195
+ verbose: args.verbose,
73196
+ manualTest,
73197
+ statesDir,
73198
+ tasksDir,
73199
+ changeStore: new OpenSpecChangeStore
73200
+ }
73201
+ }, undefined, false, undefined, this);
73202
+ }
73155
73203
  function App2({ args, statesDir, tasksDir, projectRoot }) {
73156
73204
  switch (args.mode) {
73157
73205
  case "list":
@@ -73203,23 +73251,11 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
73203
73251
  message: "Error: --name is required for task mode"
73204
73252
  }, undefined, false, undefined, this);
73205
73253
  }
73206
- return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskLoop, {
73207
- opts: {
73208
- name: args.name,
73209
- prompt: args.prompt,
73210
- engine: args.engine,
73211
- model: args.model,
73212
- maxIterations: args.maxIterations,
73213
- maxCostUsd: args.maxCostUsd,
73214
- maxRuntimeMinutes: args.maxRuntimeMinutes,
73215
- maxConsecutiveFailures: args.maxConsecutiveFailures,
73216
- delay: args.delay,
73217
- log: args.log,
73218
- verbose: args.verbose,
73219
- statesDir,
73220
- tasksDir,
73221
- changeStore: new OpenSpecChangeStore
73222
- }
73254
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskModeWrapper, {
73255
+ args,
73256
+ statesDir,
73257
+ tasksDir,
73258
+ projectRoot
73223
73259
  }, undefined, false, undefined, this);
73224
73260
  }
73225
73261
  }
@@ -73236,7 +73272,7 @@ async function findProjectRoot() {
73236
73272
  while (dir !== "/") {
73237
73273
  if (await exists2(join19(dir, "openspec")))
73238
73274
  return dir;
73239
- dir = resolve(dir, "..");
73275
+ dir = resolve2(dir, "..");
73240
73276
  }
73241
73277
  return process.cwd();
73242
73278
  }
package/dist/mcp/index.js CHANGED
@@ -23966,6 +23966,7 @@ var StateSchema = exports_external.object({
23966
23966
  lastModified: exports_external.string(),
23967
23967
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
23968
23968
  model: exports_external.string().default("opus"),
23969
+ manualTest: exports_external.boolean().default(false),
23969
23970
  usage: UsageSchema.default({}),
23970
23971
  history: exports_external.array(HistoryEntrySchema).default([]),
23971
23972
  metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
@@ -24031,6 +24032,7 @@ function buildInitialState(options) {
24031
24032
  prompt: options.prompt,
24032
24033
  engine: options.engine ?? "claude",
24033
24034
  model: options.model ?? "opus",
24035
+ manualTest: options.manualTest ?? false,
24034
24036
  createdAt: now,
24035
24037
  lastModified: now,
24036
24038
  metadata: { branch }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.13.7",
3
+ "version": "2.13.10",
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",