@caseyharalson/orrery 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HELP.md CHANGED
@@ -78,10 +78,13 @@ Options:
78
78
  --review Enable code review loop after each step
79
79
  --parallel Enable parallel execution with git worktrees for isolation
80
80
  --background Run orchestration as a detached background process
81
+ --on-complete <command> Run a shell command when the orchestrator finishes
81
82
  ```
82
83
 
83
- Only one `exec`/`resume` can run at a time per project. A lock file
84
- (`exec.lock`) prevents concurrent runs and is automatically cleaned up.
84
+ A lock file prevents concurrent runs and is automatically cleaned up. Without
85
+ `--plan`, a global lock (`exec.lock`) allows only one execution at a time.
86
+ With `--plan`, each plan gets its own lock (`exec-<planId>.lock`) and runs in
87
+ an isolated worktree, so multiple plans can execute concurrently.
85
88
 
86
89
  Example:
87
90
 
@@ -103,17 +106,21 @@ Options:
103
106
  --step <id> Unblock a specific step before resuming
104
107
  --all Unblock all blocked steps (default behavior)
105
108
  --dry-run Preview what would be unblocked without making changes
109
+ --background Run resume as a detached background process
110
+ --on-complete <command> Run a shell command when the orchestrator finishes
106
111
  ```
107
112
 
108
- When `--plan` is provided, the plan's `work_branch` must match the current
109
- branch. If the plan hasn't been dispatched yet (no `work_branch`), use
110
- `orrery exec --plan` first.
113
+ When `--plan` is provided and a worktree exists for the plan, resume runs
114
+ directly inside the worktree. Otherwise, the plan's `work_branch` must match
115
+ the current branch. If the plan hasn't been dispatched yet (no `work_branch`),
116
+ use `orrery exec --plan` first.
111
117
 
112
118
  Example:
113
119
 
114
120
  ```bash
115
121
  orrery resume
116
122
  orrery resume --plan my-feature.yaml
123
+ orrery resume --plan my-feature.yaml --background
117
124
  orrery resume --step step-2
118
125
  orrery resume --dry-run
119
126
  ```
@@ -264,16 +271,69 @@ max iteration limit is reached (default: 3).
264
271
 
265
272
  ### Background Execution
266
273
 
267
- Run orchestration as a detached background process:
274
+ Run orchestration or resume as a detached background process:
268
275
 
269
276
  ```bash
270
277
  orrery exec --background
271
278
  orrery exec --plan my-feature.yaml --background
279
+ orrery resume --plan my-feature.yaml --background
272
280
  ```
273
281
 
274
- The process runs detached and logs output to `<work-dir>/exec.log`. Use
275
- `orrery status` to check progress. A lock file prevents starting a second
276
- execution while one is already running.
282
+ The process runs detached and logs output to `<work-dir>/exec.log` (or
283
+ `exec-<planId>.log` for per-plan execution). Use `orrery status` to check
284
+ progress. Each execution acquires its own lock file — a global `exec.lock` for non-plan
285
+ runs, or `exec-<planId>.lock` when `--plan` is used — so background per-plan
286
+ executions can run concurrently.
287
+
288
+ ### Concurrent Plan Execution
289
+
290
+ Run multiple plans at the same time by using `--plan` in separate terminals.
291
+ Each plan executes in its own git worktree (`.worktrees/plan-<planId>`) with
292
+ an independent lock file, so plans do not interfere with each other.
293
+
294
+ ```bash
295
+ # Terminal 1
296
+ orrery exec --plan auth-feature.yaml
297
+
298
+ # Terminal 2
299
+ orrery exec --plan search-feature.yaml
300
+
301
+ # Or run both in the background from a single terminal
302
+ orrery exec --plan auth-feature.yaml --background
303
+ orrery exec --plan search-feature.yaml --background
304
+ ```
305
+
306
+ Use `orrery status` to see all running plans at once. Resume a specific plan
307
+ with `orrery resume --plan <file>`, which automatically re-enters the plan's
308
+ existing worktree.
309
+
310
+ Note: Without `--plan`, execution uses a global lock and only one run is
311
+ allowed at a time.
312
+
313
+ ### Completion Hook
314
+
315
+ Run a shell command when the orchestrator finishes using `--on-complete`:
316
+
317
+ ```bash
318
+ orrery exec --on-complete "notify-send 'Plan finished'"
319
+ orrery resume --plan my-feature.yaml --on-complete "./scripts/on-done.sh"
320
+ ```
321
+
322
+ The command receives plan context through environment variables:
323
+
324
+ | Variable | Description | Example |
325
+ | :----------------------- | :--------------------------------------------- | :----------------------------------------------------- |
326
+ | `ORRERY_PLAN_NAME` | Plan file name | `my-feature.yaml` |
327
+ | `ORRERY_PLAN_FILE` | Absolute path to the plan file | `/home/user/project/.agent-work/plans/my-feature.yaml` |
328
+ | `ORRERY_PLAN_OUTCOME` | Outcome: `success`, `partial`, or `incomplete` | `success` |
329
+ | `ORRERY_WORK_BRANCH` | Work branch name | `plan/my-feature` |
330
+ | `ORRERY_SOURCE_BRANCH` | Source branch name | `main` |
331
+ | `ORRERY_PR_URL` | PR URL (empty if not created) | `https://github.com/user/repo/pull/42` |
332
+ | `ORRERY_STEPS_TOTAL` | Total number of steps | `5` |
333
+ | `ORRERY_STEPS_COMPLETED` | Number of completed steps | `4` |
334
+ | `ORRERY_STEPS_BLOCKED` | Number of blocked steps | `1` |
335
+
336
+ Hook failures are logged but do not fail the orchestrator.
277
337
 
278
338
  ### Parallel Execution
279
339
 
@@ -393,11 +453,13 @@ Orrery maintains state in `.agent-work/` (configurable via `ORRERY_WORK_DIR`):
393
453
 
394
454
  ```
395
455
  .agent-work/
396
- plans/ Active plan files (new and in-progress)
397
- reports/ Step-level execution logs and outcomes
398
- completed/ Successfully executed plans (archived)
399
- exec.lock Lock file when orchestration is running
400
- exec.log Output log for background execution
456
+ plans/ Active plan files (new and in-progress)
457
+ reports/ Step-level execution logs and outcomes
458
+ completed/ Successfully executed plans (archived)
459
+ exec.lock Lock file when orchestration is running
460
+ exec-<planId>.lock Per-plan lock file for concurrent execution
461
+ exec.log Output log for background execution
462
+ exec-<planId>.log Per-plan output log for concurrent background execution
401
463
  ```
402
464
 
403
465
  When `ORRERY_WORK_DIR` is set, Orrery automatically scopes to a project
package/README.md CHANGED
@@ -89,6 +89,7 @@ For power users, Orrery offers additional capabilities:
89
89
  - **Review Loop** - Iterative code review after each step with automatic fixes
90
90
  - **Parallel Execution** - Run independent steps concurrently with git worktree isolation
91
91
  - **Background Execution** - Run orchestration as a detached process and poll status
92
+ - **Completion Hook** - Run a command when orchestration finishes
92
93
  - **Handling Blocked Plans** - Recovery workflows when steps cannot complete
93
94
 
94
95
  See [Advanced Workflows](docs/advanced-workflows.md) for details.
@@ -121,14 +122,14 @@ The Orchestrator (`orrery exec`) is the engine that drives the process. It loads
121
122
 
122
123
  ## Command Reference
123
124
 
124
- | Command | Description |
125
- | :------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- |
126
- | `orrery` | Command reference. |
127
- | `orrery init` | Initialize Orrery: install skills to detected agents. |
128
- | `orrery manual` | Show the full CLI reference manual. |
129
- | `orrery orchestrate` | Executes the active plan. Use `--review` for review loop, `--parallel` for parallel execution, `--background` for detached mode. Alias: `exec`. |
130
- | `orrery resume` | Unblock steps and resume orchestration. Use `--plan` to target a specific plan. |
131
- | `orrery status` | Shows the progress of current plans and active execution status. |
125
+ | Command | Description |
126
+ | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | `orrery` | Command reference. |
128
+ | `orrery init` | Initialize Orrery: install skills to detected agents. |
129
+ | `orrery manual` | Show the full CLI reference manual. |
130
+ | `orrery orchestrate` | Executes the active plan. Use `--review` for review loop, `--parallel` for parallel execution, `--background` for detached mode, `--on-complete` for a completion hook. Alias: `exec`. |
131
+ | `orrery resume` | Unblock steps and resume orchestration. Use `--plan` to target a specific plan, `--background` for detached mode. |
132
+ | `orrery status` | Shows the progress of current plans and active execution status. |
132
133
 
133
134
  ## Directory Structure
134
135
 
@@ -4,6 +4,7 @@ const path = require("path");
4
4
 
5
5
  const { orchestrate } = require("../../orchestration");
6
6
  const { getWorkDir } = require("../../utils/paths");
7
+ const { derivePlanId } = require("../../utils/git");
7
8
 
8
9
  module.exports = function registerOrchestrateCommand(program) {
9
10
  program
@@ -23,6 +24,10 @@ module.exports = function registerOrchestrateCommand(program) {
23
24
  "--background",
24
25
  "Run orchestration as a detached background process"
25
26
  )
27
+ .option(
28
+ "--on-complete <command>",
29
+ "Run a shell command when the orchestrator finishes"
30
+ )
26
31
  .action(async (options) => {
27
32
  // Background mode: re-spawn as detached process
28
33
  if (options.background) {
@@ -38,8 +43,15 @@ module.exports = function registerOrchestrateCommand(program) {
38
43
  if (options.resume) args.push("--resume");
39
44
  if (options.review) args.push("--review");
40
45
  if (options.parallel) args.push("--parallel");
46
+ if (options.onComplete)
47
+ args.push("--on-complete", options.onComplete);
41
48
 
42
- const logFile = path.join(getWorkDir(), "exec.log");
49
+ let logFileName = "exec.log";
50
+ if (options.plan) {
51
+ const planId = derivePlanId(path.basename(options.plan));
52
+ logFileName = `exec-${planId}.log`;
53
+ }
54
+ const logFile = path.join(getWorkDir(), logFileName);
43
55
  const logFd = fs.openSync(logFile, "a");
44
56
 
45
57
  const binPath = path.join(
@@ -74,7 +86,8 @@ module.exports = function registerOrchestrateCommand(program) {
74
86
  verbose: options.verbose,
75
87
  resume: options.resume,
76
88
  review: options.review,
77
- parallel: options.parallel
89
+ parallel: options.parallel,
90
+ onComplete: options.onComplete
78
91
  });
79
92
  } catch (error) {
80
93
  console.error(error && error.message ? error.message : error);
@@ -1,3 +1,4 @@
1
+ const { spawn } = require("child_process");
1
2
  const fs = require("fs");
2
3
  const path = require("path");
3
4
 
@@ -6,8 +7,8 @@ const {
6
7
  loadPlan,
7
8
  updateStepsStatus
8
9
  } = require("../../orchestration/plan-loader");
9
- const { commit, getCurrentBranch } = require("../../utils/git");
10
- const { getPlansDir } = require("../../utils/paths");
10
+ const { commit, getCurrentBranch, derivePlanId } = require("../../utils/git");
11
+ const { getPlansDir, getWorkDir } = require("../../utils/paths");
11
12
  const { orchestrate } = require("../../orchestration");
12
13
 
13
14
  function supportsColor() {
@@ -36,7 +37,60 @@ module.exports = function registerResumeCommand(program) {
36
37
  "--dry-run",
37
38
  "Preview what would be unblocked without making changes"
38
39
  )
40
+ .option("--background", "Run resume as a detached background process")
41
+ .option(
42
+ "--on-complete <command>",
43
+ "Run a shell command when the orchestrator finishes"
44
+ )
39
45
  .action(async (options) => {
46
+ // Background mode: re-spawn as detached process
47
+ if (options.background) {
48
+ if (options.dryRun) {
49
+ console.log(
50
+ "Note: --background with --dry-run runs in foreground.\n"
51
+ );
52
+ // Fall through to normal execution
53
+ } else {
54
+ const args = [];
55
+ if (options.plan) args.push("--plan", options.plan);
56
+ if (options.step) args.push("--step", options.step);
57
+ if (options.all) args.push("--all");
58
+ if (options.onComplete)
59
+ args.push("--on-complete", options.onComplete);
60
+
61
+ let logFileName = "exec.log";
62
+ if (options.plan) {
63
+ const planId = derivePlanId(path.basename(options.plan));
64
+ logFileName = `exec-${planId}.log`;
65
+ }
66
+ const logFile = path.join(getWorkDir(), logFileName);
67
+ const logFd = fs.openSync(logFile, "a");
68
+
69
+ const binPath = path.join(
70
+ __dirname,
71
+ "..",
72
+ "..",
73
+ "..",
74
+ "bin",
75
+ "orrery.js"
76
+ );
77
+ const child = spawn(process.execPath, [binPath, "resume", ...args], {
78
+ detached: true,
79
+ stdio: ["ignore", logFd, logFd],
80
+ cwd: process.cwd(),
81
+ env: process.env
82
+ });
83
+
84
+ child.unref();
85
+ fs.closeSync(logFd);
86
+
87
+ console.log(`Background resume started (PID ${child.pid})`);
88
+ console.log(`Log file: ${logFile}`);
89
+ console.log("\nUse 'orrery status' to check progress.");
90
+ return;
91
+ }
92
+ }
93
+
40
94
  let planFile;
41
95
  let plan;
42
96
 
@@ -73,23 +127,34 @@ module.exports = function registerResumeCommand(program) {
73
127
  return;
74
128
  }
75
129
 
76
- // Verify current branch matches plan's work_branch
77
- let currentBranch;
78
- try {
79
- currentBranch = getCurrentBranch(process.cwd());
80
- } catch {
81
- console.error("Error detecting current branch.");
82
- process.exitCode = 1;
83
- return;
84
- }
130
+ // Check for existing worktree if found, skip branch validation
131
+ const planId = derivePlanId(path.basename(resolvedPath));
132
+ const worktreePath = path.join(
133
+ process.cwd(),
134
+ ".worktrees",
135
+ `plan-${planId}`
136
+ );
137
+ const hasWorktree = require("fs").existsSync(worktreePath);
85
138
 
86
- if (currentBranch !== plan.metadata.work_branch) {
87
- console.error(
88
- `Plan expects branch '${plan.metadata.work_branch}' but you are on '${currentBranch}'.`
89
- );
90
- console.log(`\nRun: git checkout ${plan.metadata.work_branch}`);
91
- process.exitCode = 1;
92
- return;
139
+ if (!hasWorktree) {
140
+ // Verify current branch matches plan's work_branch
141
+ let currentBranch;
142
+ try {
143
+ currentBranch = getCurrentBranch(process.cwd());
144
+ } catch {
145
+ console.error("Error detecting current branch.");
146
+ process.exitCode = 1;
147
+ return;
148
+ }
149
+
150
+ if (currentBranch !== plan.metadata.work_branch) {
151
+ console.error(
152
+ `Plan expects branch '${plan.metadata.work_branch}' but you are on '${currentBranch}'.`
153
+ );
154
+ console.log(`\nRun: git checkout ${plan.metadata.work_branch}`);
155
+ process.exitCode = 1;
156
+ return;
157
+ }
93
158
  }
94
159
  } else {
95
160
  // 1. Find plan for current branch (existing behavior)
@@ -137,7 +202,11 @@ module.exports = function registerResumeCommand(program) {
137
202
  }
138
203
 
139
204
  console.log("Resuming orchestration...\n");
140
- await orchestrate({ resume: true, plan: options.plan });
205
+ await orchestrate({
206
+ resume: true,
207
+ plan: options.plan,
208
+ onComplete: options.onComplete
209
+ });
141
210
  return;
142
211
  }
143
212
 
@@ -204,6 +273,10 @@ module.exports = function registerResumeCommand(program) {
204
273
 
205
274
  // 8. Resume orchestration
206
275
  console.log("\nResuming orchestration...\n");
207
- await orchestrate({ resume: true, plan: options.plan });
276
+ await orchestrate({
277
+ resume: true,
278
+ plan: options.plan,
279
+ onComplete: options.onComplete
280
+ });
208
281
  });
209
282
  };
@@ -1,10 +1,15 @@
1
+ const fs = require("fs");
1
2
  const path = require("path");
2
3
 
3
- const { getPlansDir } = require("../../utils/paths");
4
+ const {
5
+ getPlansDir,
6
+ getCompletedDir,
7
+ getWorkDir
8
+ } = require("../../utils/paths");
4
9
  const { getPlanFiles, loadPlan } = require("../../orchestration/plan-loader");
5
10
  const { findPlanForCurrentBranch } = require("../../utils/plan-detect");
6
- const { getCurrentBranch } = require("../../utils/git");
7
- const { getLockStatus } = require("../../utils/lock");
11
+ const { getCurrentBranch, derivePlanId } = require("../../utils/git");
12
+ const { getLockStatus, listPlanLocks } = require("../../utils/lock");
8
13
 
9
14
  function supportsColor() {
10
15
  return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
@@ -48,7 +53,21 @@ function resolvePlanPath(planArg) {
48
53
  if (!planArg) return null;
49
54
  if (path.isAbsolute(planArg)) return planArg;
50
55
  if (planArg.includes(path.sep)) return path.resolve(process.cwd(), planArg);
51
- return path.join(getPlansDir(), planArg);
56
+
57
+ // Build candidate filenames: exact name, then with .yaml/.yml appended
58
+ const names = [planArg];
59
+ if (!planArg.endsWith(".yaml") && !planArg.endsWith(".yml")) {
60
+ names.push(`${planArg}.yaml`, `${planArg}.yml`);
61
+ }
62
+
63
+ // Check active plans first, then completed
64
+ for (const dir of [getPlansDir(), getCompletedDir()]) {
65
+ for (const name of names) {
66
+ const candidate = path.join(dir, name);
67
+ if (fs.existsSync(candidate)) return candidate;
68
+ }
69
+ }
70
+ return path.join(getPlansDir(), planArg); // default so caller's error message still works
52
71
  }
53
72
 
54
73
  function summarizePlans(plans) {
@@ -76,6 +95,14 @@ function renderPlanList(plans) {
76
95
  function renderPlanDetail(plan) {
77
96
  const status = getPlanStatus(plan);
78
97
  console.log(`${formatStatusLabel(status)} ${plan.fileName}`);
98
+ if (plan.metadata.completed_at) {
99
+ console.log(` Completed: ${plan.metadata.completed_at}`);
100
+ }
101
+ const planId = derivePlanId(plan.fileName);
102
+ const logFile = path.join(getWorkDir(), `exec-${planId}.log`);
103
+ if (fs.existsSync(logFile)) {
104
+ console.log(` Log: ${logFile}`);
105
+ }
79
106
  for (const step of plan.steps) {
80
107
  const stepLabel = formatStatusLabel(step.status || "pending");
81
108
  const description = step.description ? ` - ${step.description}` : "";
@@ -93,7 +120,7 @@ module.exports = function registerStatusCommand(program) {
93
120
  .description("Show orchestration status for plans in the current project")
94
121
  .option("--plan <file>", "Show detailed status for a specific plan")
95
122
  .action((options) => {
96
- // Check for active execution
123
+ // Check for active global execution
97
124
  const lock = getLockStatus();
98
125
  if (lock.locked) {
99
126
  console.log(
@@ -105,12 +132,32 @@ module.exports = function registerStatusCommand(program) {
105
132
  );
106
133
  }
107
134
 
135
+ // Check for per-plan executions
136
+ const planLocks = listPlanLocks();
137
+ const activePlanLocks = planLocks.filter((l) => l.active);
138
+ const stalePlanLocks = planLocks.filter((l) => l.stale);
139
+
140
+ if (activePlanLocks.length > 0) {
141
+ console.log(`Active plan executions (${activePlanLocks.length}):`);
142
+ for (const pl of activePlanLocks) {
143
+ let line = ` - ${pl.planId} (PID ${pl.pid}, started ${pl.startedAt})`;
144
+ if (pl.worktreePath) line += `\n worktree: ${pl.worktreePath}`;
145
+ console.log(line);
146
+ }
147
+ console.log();
148
+ }
149
+ if (stalePlanLocks.length > 0) {
150
+ console.log(
151
+ `Note: ${stalePlanLocks.length} stale per-plan lock(s) detected\n`
152
+ );
153
+ }
154
+
108
155
  const plansDir = getPlansDir();
109
156
  const planArg = options.plan;
110
157
 
111
158
  if (planArg) {
112
159
  const planPath = resolvePlanPath(planArg);
113
- if (!planPath || !require("fs").existsSync(planPath)) {
160
+ if (!planPath || !fs.existsSync(planPath)) {
114
161
  console.error(`Plan not found: ${planArg}`);
115
162
  process.exitCode = 1;
116
163
  return;
@@ -135,16 +182,28 @@ module.exports = function registerStatusCommand(program) {
135
182
  }
136
183
 
137
184
  const planFiles = getPlanFiles(plansDir);
138
- if (planFiles.length === 0) {
185
+ const completedDir = getCompletedDir();
186
+ const completedFiles = getPlanFiles(completedDir);
187
+
188
+ if (planFiles.length === 0 && completedFiles.length === 0) {
139
189
  console.log("No plans found");
140
190
  return;
141
191
  }
142
192
 
143
- const plans = planFiles.map((planFile) => loadPlan(planFile));
144
- const { pendingSteps, completedSteps } = summarizePlans(plans);
145
- console.log(
146
- `${plans.length} plans, ${pendingSteps} pending steps, ${completedSteps} completed`
147
- );
148
- renderPlanList(plans);
193
+ if (planFiles.length > 0) {
194
+ const plans = planFiles.map((planFile) => loadPlan(planFile));
195
+ const { pendingSteps, completedSteps } = summarizePlans(plans);
196
+ console.log(
197
+ `${plans.length} plans, ${pendingSteps} pending steps, ${completedSteps} completed`
198
+ );
199
+ renderPlanList(plans);
200
+ }
201
+
202
+ if (completedFiles.length > 0) {
203
+ if (planFiles.length > 0) console.log();
204
+ console.log(`Completed (${completedFiles.length}):`);
205
+ const completedPlans = completedFiles.map((f) => loadPlan(f));
206
+ renderPlanList(completedPlans);
207
+ }
149
208
  });
150
209
  };
@@ -251,14 +251,28 @@ function createDefaultResult(stepId, exitCode, stderr) {
251
251
  }
252
252
 
253
253
  /**
254
- * Check if an error condition should trigger failover to another agent
254
+ * Check if agent stdout contains valid structured output (indicating a
255
+ * legitimate task-level result rather than an infrastructure failure).
256
+ * @param {string} stdout - Raw stdout from agent
257
+ * @returns {boolean}
258
+ */
259
+ function hasValidAgentOutput(stdout) {
260
+ if (!stdout) return false;
261
+ const results = parseAgentResults(stdout);
262
+ return results.length > 0;
263
+ }
264
+
265
+ /**
266
+ * Check if an error condition should trigger failover to another agent.
267
+ * Failover triggers on any non-zero exit unless the agent produced valid
268
+ * structured output (indicating a legitimate task-level failure, not an
269
+ * infrastructure issue).
255
270
  * @param {Object} result - Process result with exitCode, stdout, stderr
256
271
  * @param {Error} spawnError - Error from spawn (if any)
257
272
  * @param {boolean} timedOut - Whether the process timed out
258
- * @param {Object} errorPatterns - Regex patterns for error detection
259
273
  * @returns {{shouldFailover: boolean, reason: string}}
260
274
  */
261
- function shouldTriggerFailover(result, spawnError, timedOut, errorPatterns) {
275
+ function shouldTriggerFailover(result, spawnError, timedOut) {
262
276
  // 1. Spawn failures (command not found, ENOENT)
263
277
  if (spawnError) {
264
278
  if (spawnError.code === "ENOENT") {
@@ -272,23 +286,12 @@ function shouldTriggerFailover(result, spawnError, timedOut, errorPatterns) {
272
286
  return { shouldFailover: true, reason: "timeout" };
273
287
  }
274
288
 
275
- // 3. Non-zero exit with error patterns in stderr (but NOT legitimate blocked)
289
+ // 3. Non-zero exit failover unless agent produced valid structured output
276
290
  if (result && result.exitCode !== 0) {
277
- const stderr = result.stderr || "";
278
-
279
- // Check API error patterns
280
- for (const pattern of errorPatterns.apiError || []) {
281
- if (pattern.test(stderr)) {
282
- return { shouldFailover: true, reason: "api_error" };
283
- }
284
- }
285
-
286
- // Check token limit patterns
287
- for (const pattern of errorPatterns.tokenLimit || []) {
288
- if (pattern.test(stderr)) {
289
- return { shouldFailover: true, reason: "token_limit" };
290
- }
291
+ if (hasValidAgentOutput(result.stdout)) {
292
+ return { shouldFailover: false, reason: null };
291
293
  }
294
+ return { shouldFailover: true, reason: "agent_error" };
292
295
  }
293
296
 
294
297
  return { shouldFailover: false, reason: null };
@@ -513,8 +516,7 @@ function invokeAgentWithFailover(
513
516
  const { shouldFailover, reason } = shouldTriggerFailover(
514
517
  result,
515
518
  null,
516
- result.timedOut,
517
- failoverConfig.errorPatterns || {}
519
+ result.timedOut
518
520
  );
519
521
 
520
522
  if (shouldFailover && i < availableAgents.length - 1) {
@@ -535,8 +537,7 @@ function invokeAgentWithFailover(
535
537
  const { shouldFailover, reason } = shouldTriggerFailover(
536
538
  null,
537
539
  spawnError,
538
- false,
539
- failoverConfig.errorPatterns || {}
540
+ false
540
541
  );
541
542
 
542
543
  if (shouldFailover && i < availableAgents.length - 1) {
@@ -595,6 +596,7 @@ module.exports = {
595
596
  invokeAgentWithFailover,
596
597
  parseAgentResults,
597
598
  createDefaultResult,
599
+ shouldTriggerFailover,
598
600
  waitForAll,
599
601
  waitForAny
600
602
  };
@@ -146,30 +146,7 @@ module.exports = {
146
146
 
147
147
  // Timeout in milliseconds before trying next agent (15 minutes)
148
148
  // Can be overridden via ORRERY_AGENT_TIMEOUT environment variable
149
- timeoutMs: 900000,
150
-
151
- // Patterns to detect failover-triggering errors from stderr
152
- errorPatterns: {
153
- // API/connection errors
154
- apiError: [
155
- /API error/i,
156
- /connection refused/i,
157
- /ECONNRESET/i,
158
- /ETIMEDOUT/i,
159
- /network error/i,
160
- /rate limit/i,
161
- /429/,
162
- /502/,
163
- /503/
164
- ],
165
- // Token/context limit errors
166
- tokenLimit: [
167
- /token limit/i,
168
- /context.*(limit|length|exceeded)/i,
169
- /maximum.*tokens/i,
170
- /too long/i
171
- ]
172
- }
149
+ timeoutMs: 900000
173
150
  },
174
151
 
175
152
  // Concurrency control