@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 +76 -14
- package/README.md +9 -8
- package/lib/cli/commands/orchestrate.js +15 -2
- package/lib/cli/commands/resume.js +93 -20
- package/lib/cli/commands/status.js +72 -13
- package/lib/orchestration/agent-invoker.js +24 -22
- package/lib/orchestration/config.js +1 -24
- package/lib/orchestration/index.js +625 -26
- package/lib/utils/git.js +43 -0
- package/lib/utils/lock.js +81 -16
- package/lib/utils/paths.js +20 -4
- package/package.json +1 -1
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
|
-
|
|
84
|
-
(`exec.lock`)
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
275
|
-
`
|
|
276
|
-
execution
|
|
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/
|
|
397
|
-
reports/
|
|
398
|
-
completed/
|
|
399
|
-
exec.lock
|
|
400
|
-
exec
|
|
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
|
-
|
|
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
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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({
|
|
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({
|
|
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 {
|
|
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
|
-
|
|
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 || !
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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
|
|
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
|
|
289
|
+
// 3. Non-zero exit — failover unless agent produced valid structured output
|
|
276
290
|
if (result && result.exitCode !== 0) {
|
|
277
|
-
|
|
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
|