@caseyharalson/orrery 0.10.0 → 0.12.0
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 +429 -0
- package/README.md +10 -7
- package/agent/skills/discovery/SKILL.md +13 -5
- package/agent/skills/orrery-execute/SKILL.md +1 -1
- package/agent/skills/refine-plan/SKILL.md +3 -2
- package/agent/skills/simulate-plan/SKILL.md +3 -2
- package/lib/cli/commands/manual.js +22 -0
- package/lib/cli/commands/orchestrate.js +52 -0
- package/lib/cli/commands/plans-dir.js +10 -0
- package/lib/cli/commands/resume.js +96 -33
- package/lib/cli/commands/status.js +13 -0
- package/lib/cli/index.js +4 -0
- package/lib/orchestration/index.js +220 -144
- package/lib/utils/git.js +9 -2
- package/lib/utils/lock.js +170 -0
- package/lib/utils/paths.js +29 -2
- package/package.json +4 -2
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
1
2
|
const path = require("path");
|
|
2
3
|
|
|
3
4
|
const { findPlanForCurrentBranch } = require("../../utils/plan-detect");
|
|
4
|
-
const {
|
|
5
|
-
|
|
5
|
+
const {
|
|
6
|
+
loadPlan,
|
|
7
|
+
updateStepsStatus
|
|
8
|
+
} = require("../../orchestration/plan-loader");
|
|
9
|
+
const { commit, getCurrentBranch } = require("../../utils/git");
|
|
10
|
+
const { getPlansDir } = require("../../utils/paths");
|
|
6
11
|
const { orchestrate } = require("../../orchestration");
|
|
7
12
|
|
|
8
13
|
function supportsColor() {
|
|
@@ -24,6 +29,7 @@ module.exports = function registerResumeCommand(program) {
|
|
|
24
29
|
program
|
|
25
30
|
.command("resume")
|
|
26
31
|
.description("Unblock steps and resume orchestration")
|
|
32
|
+
.option("--plan <file>", "Resume a specific plan file")
|
|
27
33
|
.option("--step <id>", "Unblock a specific step before resuming")
|
|
28
34
|
.option("--all", "Unblock all blocked steps (default behavior)")
|
|
29
35
|
.option(
|
|
@@ -31,31 +37,90 @@ module.exports = function registerResumeCommand(program) {
|
|
|
31
37
|
"Preview what would be unblocked without making changes"
|
|
32
38
|
)
|
|
33
39
|
.action(async (options) => {
|
|
34
|
-
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
let planFile;
|
|
41
|
+
let plan;
|
|
42
|
+
|
|
43
|
+
if (options.plan) {
|
|
44
|
+
// Resolve plan path (same pattern as status.js)
|
|
45
|
+
const planArg = options.plan;
|
|
46
|
+
let resolvedPath;
|
|
47
|
+
if (path.isAbsolute(planArg)) {
|
|
48
|
+
resolvedPath = planArg;
|
|
49
|
+
} else if (planArg.includes(path.sep)) {
|
|
50
|
+
resolvedPath = path.resolve(process.cwd(), planArg);
|
|
51
|
+
} else {
|
|
52
|
+
resolvedPath = path.join(getPlansDir(), planArg);
|
|
53
|
+
}
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
56
|
+
console.error(`Plan not found: ${planArg}`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
plan = loadPlan(resolvedPath);
|
|
62
|
+
planFile = resolvedPath;
|
|
63
|
+
|
|
64
|
+
// Validate work_branch
|
|
65
|
+
if (!plan.metadata.work_branch) {
|
|
66
|
+
console.error(
|
|
67
|
+
"Plan has no work_branch — it hasn't been dispatched yet."
|
|
68
|
+
);
|
|
69
|
+
console.log(
|
|
70
|
+
"\nUse 'orrery exec --plan <file>' to dispatch the plan first."
|
|
71
|
+
);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
+
}
|
|
85
|
+
|
|
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;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// 1. Find plan for current branch (existing behavior)
|
|
96
|
+
let match;
|
|
97
|
+
try {
|
|
98
|
+
match = findPlanForCurrentBranch();
|
|
99
|
+
} catch {
|
|
100
|
+
console.error("Error detecting plan from current branch.");
|
|
101
|
+
console.log(
|
|
102
|
+
"Make sure you're on a work branch (e.g., plan/feature-name)."
|
|
103
|
+
);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
57
107
|
|
|
58
|
-
|
|
108
|
+
if (!match) {
|
|
109
|
+
console.error(
|
|
110
|
+
"Not on a work branch. No plan found for current branch."
|
|
111
|
+
);
|
|
112
|
+
console.log("\nTo resume a plan:");
|
|
113
|
+
console.log(" 1. git checkout <work-branch>");
|
|
114
|
+
console.log(" 2. orrery resume");
|
|
115
|
+
console.log("\nOr specify a plan directly:");
|
|
116
|
+
console.log(" orrery resume --plan <file>");
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
planFile = match.planFile;
|
|
122
|
+
plan = match.plan;
|
|
123
|
+
}
|
|
59
124
|
const planFileName = path.basename(planFile);
|
|
60
125
|
console.log(`(detected plan: ${planFileName})\n`);
|
|
61
126
|
|
|
@@ -72,13 +137,11 @@ module.exports = function registerResumeCommand(program) {
|
|
|
72
137
|
}
|
|
73
138
|
|
|
74
139
|
console.log("Resuming orchestration...\n");
|
|
75
|
-
await orchestrate({ resume: true });
|
|
140
|
+
await orchestrate({ resume: true, plan: options.plan });
|
|
76
141
|
return;
|
|
77
142
|
}
|
|
78
143
|
|
|
79
144
|
// 4. Determine which steps to unblock
|
|
80
|
-
let stepsToUnblock = [];
|
|
81
|
-
|
|
82
145
|
if (options.step) {
|
|
83
146
|
// Unblock specific step
|
|
84
147
|
const step = blockedSteps.find((s) => s.id === options.step);
|
|
@@ -93,12 +156,12 @@ module.exports = function registerResumeCommand(program) {
|
|
|
93
156
|
process.exitCode = 1;
|
|
94
157
|
return;
|
|
95
158
|
}
|
|
96
|
-
stepsToUnblock = [step];
|
|
97
|
-
} else {
|
|
98
|
-
// Default: unblock all (--all is implicit)
|
|
99
|
-
stepsToUnblock = blockedSteps;
|
|
100
159
|
}
|
|
101
160
|
|
|
161
|
+
const stepsToUnblock = options.step
|
|
162
|
+
? [blockedSteps.find((s) => s.id === options.step)]
|
|
163
|
+
: blockedSteps;
|
|
164
|
+
|
|
102
165
|
// 5. Dry-run mode: show preview
|
|
103
166
|
if (options.dryRun) {
|
|
104
167
|
console.log("Dry run - would unblock the following steps:\n");
|
|
@@ -141,6 +204,6 @@ module.exports = function registerResumeCommand(program) {
|
|
|
141
204
|
|
|
142
205
|
// 8. Resume orchestration
|
|
143
206
|
console.log("\nResuming orchestration...\n");
|
|
144
|
-
await orchestrate({ resume: true });
|
|
207
|
+
await orchestrate({ resume: true, plan: options.plan });
|
|
145
208
|
});
|
|
146
209
|
};
|
|
@@ -4,6 +4,7 @@ const { getPlansDir } = require("../../utils/paths");
|
|
|
4
4
|
const { getPlanFiles, loadPlan } = require("../../orchestration/plan-loader");
|
|
5
5
|
const { findPlanForCurrentBranch } = require("../../utils/plan-detect");
|
|
6
6
|
const { getCurrentBranch } = require("../../utils/git");
|
|
7
|
+
const { getLockStatus } = require("../../utils/lock");
|
|
7
8
|
|
|
8
9
|
function supportsColor() {
|
|
9
10
|
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
@@ -92,6 +93,18 @@ module.exports = function registerStatusCommand(program) {
|
|
|
92
93
|
.description("Show orchestration status for plans in the current project")
|
|
93
94
|
.option("--plan <file>", "Show detailed status for a specific plan")
|
|
94
95
|
.action((options) => {
|
|
96
|
+
// Check for active execution
|
|
97
|
+
const lock = getLockStatus();
|
|
98
|
+
if (lock.locked) {
|
|
99
|
+
console.log(
|
|
100
|
+
`Execution in progress (PID ${lock.pid}, started ${lock.startedAt})\n`
|
|
101
|
+
);
|
|
102
|
+
} else if (lock.stale) {
|
|
103
|
+
console.log(
|
|
104
|
+
`Note: Stale lock detected (PID ${lock.pid} no longer running)\n`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
const plansDir = getPlansDir();
|
|
96
109
|
const planArg = options.plan;
|
|
97
110
|
|
package/lib/cli/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const registerStatus = require("./commands/status");
|
|
|
9
9
|
const registerResume = require("./commands/resume");
|
|
10
10
|
const registerValidatePlan = require("./commands/validate-plan");
|
|
11
11
|
const registerIngestPlan = require("./commands/ingest-plan");
|
|
12
|
+
const registerManual = require("./commands/manual");
|
|
13
|
+
const registerPlansDir = require("./commands/plans-dir");
|
|
12
14
|
const registerHelp = require("./commands/help");
|
|
13
15
|
|
|
14
16
|
function getPackageVersion() {
|
|
@@ -34,6 +36,8 @@ function buildProgram() {
|
|
|
34
36
|
registerResume(program);
|
|
35
37
|
registerValidatePlan(program);
|
|
36
38
|
registerIngestPlan(program);
|
|
39
|
+
registerManual(program);
|
|
40
|
+
registerPlansDir(program);
|
|
37
41
|
registerHelp(program);
|
|
38
42
|
|
|
39
43
|
program.on("command:*", (operands) => {
|
|
@@ -62,7 +62,8 @@ const config = require("./config");
|
|
|
62
62
|
const {
|
|
63
63
|
getPlansDir,
|
|
64
64
|
getCompletedDir,
|
|
65
|
-
getReportsDir
|
|
65
|
+
getReportsDir,
|
|
66
|
+
isWorkDirExternal
|
|
66
67
|
} = require("../utils/paths");
|
|
67
68
|
|
|
68
69
|
const {
|
|
@@ -72,6 +73,7 @@ const {
|
|
|
72
73
|
} = require("./condensed-plan");
|
|
73
74
|
|
|
74
75
|
const { ProgressTracker } = require("./progress-tracker");
|
|
76
|
+
const { acquireLock, releaseLock } = require("../utils/lock");
|
|
75
77
|
|
|
76
78
|
const REPO_ROOT = process.cwd();
|
|
77
79
|
|
|
@@ -277,140 +279,168 @@ async function orchestrate(options = {}) {
|
|
|
277
279
|
);
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
console.log(`Source branch: ${sourceBranch}\n`);
|
|
282
|
+
// Acquire execution lock (skip for dry-run)
|
|
283
|
+
if (!normalizedOptions.dryRun) {
|
|
284
|
+
const lockResult = acquireLock();
|
|
285
|
+
if (!lockResult.acquired) {
|
|
286
|
+
console.error(`Cannot start: ${lockResult.reason}`);
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
289
290
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
process.
|
|
291
|
+
// Clean up lock on signals
|
|
292
|
+
const cleanupLock = () => {
|
|
293
|
+
releaseLock();
|
|
294
|
+
process.exit();
|
|
295
|
+
};
|
|
296
|
+
process.on("SIGINT", cleanupLock);
|
|
297
|
+
process.on("SIGTERM", cleanupLock);
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
await handleResumeMode(plansDir, completedDir, reportsDir, sourceBranch);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
300
|
+
try {
|
|
301
|
+
console.log("=== Plan Orchestrator Starting ===\n");
|
|
303
302
|
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
const plansDir = getPlansDir();
|
|
304
|
+
const completedDir = getCompletedDir();
|
|
305
|
+
const reportsDir = getReportsDir();
|
|
306
306
|
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
// Get list of completed plan filenames (to exclude)
|
|
308
|
+
const completedNames = getCompletedPlanNames(completedDir);
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
310
|
+
let planFiles = [];
|
|
311
|
+
let allPlanFiles = [];
|
|
312
|
+
|
|
313
|
+
if (normalizedOptions.plan) {
|
|
314
|
+
const resolvedPlanFile = resolvePlanFile(
|
|
315
|
+
normalizedOptions.plan,
|
|
316
|
+
plansDir
|
|
317
|
+
);
|
|
318
|
+
if (!resolvedPlanFile) {
|
|
319
|
+
console.error(`Plan file not found: ${normalizedOptions.plan}`);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
if (completedNames.has(path.basename(resolvedPlanFile))) {
|
|
323
|
+
console.log(
|
|
324
|
+
`Plan already completed: ${path.basename(resolvedPlanFile)}`
|
|
325
|
+
);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
allPlanFiles = [resolvedPlanFile];
|
|
329
|
+
} else {
|
|
330
|
+
// Scan for active plans
|
|
331
|
+
allPlanFiles = getPlanFiles(plansDir).filter(
|
|
332
|
+
(f) => !completedNames.has(path.basename(f))
|
|
333
|
+
);
|
|
315
334
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
335
|
+
|
|
336
|
+
// Filter out plans that are already dispatched (have work_branch set)
|
|
337
|
+
const dispatchedPlans = [];
|
|
338
|
+
|
|
339
|
+
for (const planFile of allPlanFiles) {
|
|
340
|
+
const plan = loadPlan(planFile);
|
|
341
|
+
if (plan.metadata.work_branch) {
|
|
342
|
+
dispatchedPlans.push({
|
|
343
|
+
file: path.basename(planFile),
|
|
344
|
+
workBranch: plan.metadata.work_branch
|
|
345
|
+
});
|
|
346
|
+
} else {
|
|
347
|
+
planFiles.push(planFile);
|
|
348
|
+
}
|
|
319
349
|
}
|
|
320
|
-
allPlanFiles = [resolvedPlanFile];
|
|
321
|
-
} else {
|
|
322
|
-
// Scan for active plans
|
|
323
|
-
allPlanFiles = getPlanFiles(plansDir).filter(
|
|
324
|
-
(f) => !completedNames.has(path.basename(f))
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
350
|
|
|
328
|
-
|
|
329
|
-
|
|
351
|
+
if (dispatchedPlans.length > 0) {
|
|
352
|
+
console.log(
|
|
353
|
+
`Skipping ${dispatchedPlans.length} already-dispatched plan(s):`
|
|
354
|
+
);
|
|
355
|
+
for (const dp of dispatchedPlans) {
|
|
356
|
+
console.log(` - ${dp.file} (work branch: ${dp.workBranch})`);
|
|
357
|
+
}
|
|
358
|
+
console.log();
|
|
359
|
+
}
|
|
330
360
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
planFiles.push(planFile);
|
|
361
|
+
if (planFiles.length === 0) {
|
|
362
|
+
console.log(
|
|
363
|
+
`No new plans to process in ${path.relative(process.cwd(), plansDir)}/`
|
|
364
|
+
);
|
|
365
|
+
console.log(
|
|
366
|
+
"Create a plan file without work_branch metadata to get started."
|
|
367
|
+
);
|
|
368
|
+
return;
|
|
340
369
|
}
|
|
341
|
-
}
|
|
342
370
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
);
|
|
347
|
-
for (const dp of dispatchedPlans) {
|
|
348
|
-
console.log(` - ${dp.file} (work branch: ${dp.workBranch})`);
|
|
371
|
+
if (normalizedOptions.dryRun) {
|
|
372
|
+
logDryRunSummary(planFiles);
|
|
373
|
+
return;
|
|
349
374
|
}
|
|
350
|
-
console.log();
|
|
351
|
-
}
|
|
352
375
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
);
|
|
357
|
-
console.log(
|
|
358
|
-
"Create a plan file without work_branch metadata to get started."
|
|
359
|
-
);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
376
|
+
// Record the source branch we're starting from
|
|
377
|
+
const sourceBranch = getCurrentBranch(REPO_ROOT);
|
|
378
|
+
console.log(`Source branch: ${sourceBranch}\n`);
|
|
362
379
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
380
|
+
// Check for uncommitted changes
|
|
381
|
+
if (hasUncommittedChanges(REPO_ROOT)) {
|
|
382
|
+
console.error(
|
|
383
|
+
"Error: Uncommitted changes detected. Please commit or stash before running orchestrator."
|
|
384
|
+
);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
367
387
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
388
|
+
// Resume mode: find and continue the plan for the current branch
|
|
389
|
+
if (normalizedOptions.resume) {
|
|
390
|
+
await handleResumeMode(
|
|
391
|
+
plansDir,
|
|
392
|
+
completedDir,
|
|
393
|
+
reportsDir,
|
|
394
|
+
sourceBranch,
|
|
395
|
+
normalizedOptions.plan
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
373
399
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
completedDir,
|
|
380
|
-
reportsDir,
|
|
381
|
-
parallelEnabled
|
|
382
|
-
);
|
|
400
|
+
console.log(`Found ${planFiles.length} plan(s) to process:\n`);
|
|
401
|
+
for (const pf of planFiles) {
|
|
402
|
+
console.log(` - ${path.basename(pf)}`);
|
|
403
|
+
}
|
|
404
|
+
console.log();
|
|
383
405
|
|
|
384
|
-
//
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
406
|
+
// Process each plan (one at a time, with branch switching)
|
|
407
|
+
for (const planFile of planFiles) {
|
|
408
|
+
const result = await processPlanWithBranching(
|
|
409
|
+
planFile,
|
|
410
|
+
sourceBranch,
|
|
411
|
+
completedDir,
|
|
412
|
+
reportsDir,
|
|
413
|
+
parallelEnabled
|
|
414
|
+
);
|
|
388
415
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
416
|
+
if (result.isComplete && result.isSuccessful) {
|
|
417
|
+
// Plan completed successfully - return to source branch for next plan
|
|
418
|
+
const currentBranch = getCurrentBranch(REPO_ROOT);
|
|
419
|
+
if (currentBranch !== sourceBranch) {
|
|
420
|
+
console.log(`\nReturning to source branch: ${sourceBranch}`);
|
|
421
|
+
checkoutBranch(sourceBranch, REPO_ROOT);
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
// Plan is blocked - stay on work branch and stop processing
|
|
425
|
+
console.log(`\nPlan "${path.basename(planFile)}" is blocked.`);
|
|
426
|
+
console.log(`Staying on work branch: ${result.workBranch}`);
|
|
427
|
+
console.log("\nTo continue:");
|
|
428
|
+
console.log(" 1. Fix the blocked steps (orrery status)");
|
|
429
|
+
console.log(" 2. Run 'orrery resume' to unblock and continue");
|
|
430
|
+
|
|
431
|
+
// List remaining unprocessed plans
|
|
432
|
+
const remaining = planFiles.slice(planFiles.indexOf(planFile) + 1);
|
|
433
|
+
if (remaining.length > 0) {
|
|
434
|
+
console.log(`\nSkipped ${remaining.length} remaining plan(s).`);
|
|
435
|
+
}
|
|
436
|
+
break; // Stop processing
|
|
408
437
|
}
|
|
409
|
-
break; // Stop processing
|
|
410
438
|
}
|
|
411
|
-
}
|
|
412
439
|
|
|
413
|
-
|
|
440
|
+
console.log("\n=== Orchestrator Complete ===");
|
|
441
|
+
} finally {
|
|
442
|
+
if (!normalizedOptions.dryRun) releaseLock();
|
|
443
|
+
}
|
|
414
444
|
}
|
|
415
445
|
|
|
416
446
|
/**
|
|
@@ -420,36 +450,73 @@ async function handleResumeMode(
|
|
|
420
450
|
plansDir,
|
|
421
451
|
completedDir,
|
|
422
452
|
reportsDir,
|
|
423
|
-
currentBranch
|
|
453
|
+
currentBranch,
|
|
454
|
+
planFileArg
|
|
424
455
|
) {
|
|
425
456
|
console.log("=== Resume Mode ===\n");
|
|
426
|
-
console.log(`Looking for plan with work_branch: ${currentBranch}\n`);
|
|
427
457
|
|
|
428
|
-
// Get all plan files (including dispatched ones)
|
|
429
|
-
const completedNames = getCompletedPlanNames(completedDir);
|
|
430
|
-
const allPlanFiles = getPlanFiles(plansDir).filter(
|
|
431
|
-
(f) => !completedNames.has(path.basename(f))
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
// Find plan matching current branch
|
|
435
458
|
let matchingPlanFile = null;
|
|
436
459
|
let matchingPlan = null;
|
|
437
460
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
461
|
+
if (planFileArg) {
|
|
462
|
+
// Resolve the plan file from argument
|
|
463
|
+
const resolved = resolvePlanFile(planFileArg, plansDir);
|
|
464
|
+
if (!resolved) {
|
|
465
|
+
console.error(`Plan file not found: ${planFileArg}`);
|
|
466
|
+
process.exit(1);
|
|
444
467
|
}
|
|
445
|
-
}
|
|
446
468
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
469
|
+
matchingPlan = loadPlan(resolved);
|
|
470
|
+
matchingPlanFile = resolved;
|
|
471
|
+
|
|
472
|
+
// Validate work_branch
|
|
473
|
+
if (!matchingPlan.metadata.work_branch) {
|
|
474
|
+
console.error("Plan has no work_branch — it hasn't been dispatched yet.");
|
|
475
|
+
console.log(
|
|
476
|
+
"\nUse 'orrery exec --plan <file>' to dispatch the plan first."
|
|
477
|
+
);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (matchingPlan.metadata.work_branch !== currentBranch) {
|
|
482
|
+
console.error(
|
|
483
|
+
`Plan expects branch '${matchingPlan.metadata.work_branch}' but you are on '${currentBranch}'.`
|
|
484
|
+
);
|
|
485
|
+
console.log(`\nRun: git checkout ${matchingPlan.metadata.work_branch}`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
console.log(`Using specified plan: ${path.basename(resolved)}\n`);
|
|
490
|
+
} else {
|
|
491
|
+
console.log(`Looking for plan with work_branch: ${currentBranch}\n`);
|
|
492
|
+
|
|
493
|
+
// Get all plan files (including dispatched ones)
|
|
494
|
+
const completedNames = getCompletedPlanNames(completedDir);
|
|
495
|
+
const allPlanFiles = getPlanFiles(plansDir).filter(
|
|
496
|
+
(f) => !completedNames.has(path.basename(f))
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// Find plan matching current branch
|
|
500
|
+
for (const planFile of allPlanFiles) {
|
|
501
|
+
const plan = loadPlan(planFile);
|
|
502
|
+
if (plan.metadata.work_branch === currentBranch) {
|
|
503
|
+
matchingPlanFile = planFile;
|
|
504
|
+
matchingPlan = plan;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!matchingPlanFile) {
|
|
510
|
+
console.error(
|
|
511
|
+
`No plan found with work_branch matching "${currentBranch}"`
|
|
512
|
+
);
|
|
513
|
+
console.log("\nTo resume a plan:");
|
|
514
|
+
console.log(" 1. git checkout <work-branch>");
|
|
515
|
+
console.log(" 2. orrery exec --resume");
|
|
516
|
+
console.log("\nOr specify a plan directly:");
|
|
517
|
+
console.log(" orrery exec --resume --plan <file>");
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
453
520
|
}
|
|
454
521
|
|
|
455
522
|
const planFileName = path.basename(matchingPlanFile);
|
|
@@ -542,6 +609,7 @@ async function handleResumeMode(
|
|
|
542
609
|
* @param {string} completedDir - Directory for completed plans
|
|
543
610
|
* @param {string} reportsDir - Directory for reports
|
|
544
611
|
* @param {boolean} parallelEnabled - Whether parallel execution with worktrees is enabled
|
|
612
|
+
* @returns {Promise<{isComplete: boolean, isSuccessful: boolean, workBranch: string}>}
|
|
545
613
|
*/
|
|
546
614
|
async function processPlanWithBranching(
|
|
547
615
|
planFile,
|
|
@@ -563,16 +631,18 @@ async function processPlanWithBranching(
|
|
|
563
631
|
plan.metadata.work_branch = workBranch;
|
|
564
632
|
savePlan(plan);
|
|
565
633
|
|
|
566
|
-
// Commit the metadata update on source branch
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (metadataCommit) {
|
|
573
|
-
console.log(
|
|
574
|
-
`Marked plan as dispatched on ${sourceBranch} (${metadataCommit.slice(0, 7)})`
|
|
634
|
+
// Commit the metadata update on source branch (only if work dir is inside repo)
|
|
635
|
+
if (!isWorkDirExternal()) {
|
|
636
|
+
const metadataCommit = commit(
|
|
637
|
+
`chore: dispatch plan ${planFileName} to ${workBranch}`,
|
|
638
|
+
[planFile],
|
|
639
|
+
REPO_ROOT
|
|
575
640
|
);
|
|
641
|
+
if (metadataCommit) {
|
|
642
|
+
console.log(
|
|
643
|
+
`Marked plan as dispatched on ${sourceBranch} (${metadataCommit.slice(0, 7)})`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
576
646
|
}
|
|
577
647
|
|
|
578
648
|
// Step 3: Create and switch to work branch
|
|
@@ -592,6 +662,8 @@ async function processPlanWithBranching(
|
|
|
592
662
|
const isComplete = plan.isComplete();
|
|
593
663
|
|
|
594
664
|
if (isComplete) {
|
|
665
|
+
const isSuccessful = plan.isSuccessful();
|
|
666
|
+
|
|
595
667
|
// Step 6: Archive the plan (on work branch)
|
|
596
668
|
archivePlan(planFile, plan, completedDir);
|
|
597
669
|
|
|
@@ -610,6 +682,8 @@ async function processPlanWithBranching(
|
|
|
610
682
|
const prBody = generatePRBody(plan);
|
|
611
683
|
const prInfo = createPullRequest(prTitle, prBody, sourceBranch, REPO_ROOT);
|
|
612
684
|
logPullRequestInfo(prInfo);
|
|
685
|
+
|
|
686
|
+
return { isComplete: true, isSuccessful, workBranch };
|
|
613
687
|
} else {
|
|
614
688
|
// Plan not complete (still has pending steps or was interrupted)
|
|
615
689
|
// Commit any progress made
|
|
@@ -624,6 +698,8 @@ async function processPlanWithBranching(
|
|
|
624
698
|
console.log(
|
|
625
699
|
"\nPlan not complete. Work branch preserved for later continuation."
|
|
626
700
|
);
|
|
701
|
+
|
|
702
|
+
return { isComplete: false, isSuccessful: false, workBranch };
|
|
627
703
|
}
|
|
628
704
|
}
|
|
629
705
|
|