@caseyharalson/orrery 0.7.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/.devcontainer.example/Dockerfile +149 -0
- package/.devcontainer.example/devcontainer.json +61 -0
- package/.devcontainer.example/init-firewall.sh +175 -0
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/agent/skills/discovery/SKILL.md +428 -0
- package/agent/skills/discovery/schemas/plan-schema.yaml +138 -0
- package/agent/skills/orrery-execute/SKILL.md +107 -0
- package/agent/skills/orrery-report/SKILL.md +119 -0
- package/agent/skills/orrery-review/SKILL.md +105 -0
- package/agent/skills/orrery-verify/SKILL.md +105 -0
- package/agent/skills/refine-plan/SKILL.md +291 -0
- package/agent/skills/simulate-plan/SKILL.md +244 -0
- package/bin/orrery.js +5 -0
- package/lib/cli/commands/help.js +21 -0
- package/lib/cli/commands/ingest-plan.js +56 -0
- package/lib/cli/commands/init.js +21 -0
- package/lib/cli/commands/install-devcontainer.js +97 -0
- package/lib/cli/commands/install-skills.js +182 -0
- package/lib/cli/commands/orchestrate.js +27 -0
- package/lib/cli/commands/resume.js +146 -0
- package/lib/cli/commands/status.js +137 -0
- package/lib/cli/commands/validate-plan.js +288 -0
- package/lib/cli/index.js +57 -0
- package/lib/orchestration/agent-invoker.js +595 -0
- package/lib/orchestration/condensed-plan.js +128 -0
- package/lib/orchestration/config.js +213 -0
- package/lib/orchestration/dependency-resolver.js +149 -0
- package/lib/orchestration/edit-invoker.js +115 -0
- package/lib/orchestration/index.js +1065 -0
- package/lib/orchestration/plan-loader.js +212 -0
- package/lib/orchestration/progress-tracker.js +208 -0
- package/lib/orchestration/report-format.js +80 -0
- package/lib/orchestration/review-invoker.js +305 -0
- package/lib/utils/agent-detector.js +47 -0
- package/lib/utils/git.js +297 -0
- package/lib/utils/paths.js +43 -0
- package/lib/utils/plan-detect.js +24 -0
- package/lib/utils/skill-copier.js +79 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plan Orchestrator
|
|
5
|
+
*
|
|
6
|
+
* Scans .agent-work/plans/ for YAML plan files, dispatches agents to execute steps,
|
|
7
|
+
* tracks completion, and archives finished plans to .agent-work/completed/.
|
|
8
|
+
*
|
|
9
|
+
* Branch Management:
|
|
10
|
+
* - Plans are discovered on the source branch (e.g., main)
|
|
11
|
+
* - Each plan gets a dedicated work branch (e.g., plan/add-feature)
|
|
12
|
+
* - All agent work happens on the work branch
|
|
13
|
+
* - When complete, a PR is created and orchestrator returns to source branch
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const YAML = require("yaml");
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
loadPlan,
|
|
22
|
+
savePlan,
|
|
23
|
+
updateStepsStatus,
|
|
24
|
+
getPlanFiles,
|
|
25
|
+
movePlanToCompleted,
|
|
26
|
+
getCompletedPlanNames
|
|
27
|
+
} = require("./plan-loader");
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
getReadySteps,
|
|
31
|
+
partitionSteps,
|
|
32
|
+
getBlockedDependents
|
|
33
|
+
} = require("./dependency-resolver");
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
invokeAgentWithFailover,
|
|
37
|
+
parseAgentResults,
|
|
38
|
+
createDefaultResult,
|
|
39
|
+
waitForAny
|
|
40
|
+
} = require("./agent-invoker");
|
|
41
|
+
const { invokeReviewAgent } = require("./review-invoker");
|
|
42
|
+
const { invokeEditAgent } = require("./edit-invoker");
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
getCurrentBranch,
|
|
46
|
+
branchExists,
|
|
47
|
+
createBranch,
|
|
48
|
+
checkoutBranch,
|
|
49
|
+
commit,
|
|
50
|
+
createPullRequest,
|
|
51
|
+
deriveBranchName,
|
|
52
|
+
hasUncommittedChanges,
|
|
53
|
+
getUncommittedDiff
|
|
54
|
+
} = require("../utils/git");
|
|
55
|
+
|
|
56
|
+
const config = require("./config");
|
|
57
|
+
const {
|
|
58
|
+
getPlansDir,
|
|
59
|
+
getCompletedDir,
|
|
60
|
+
getReportsDir
|
|
61
|
+
} = require("../utils/paths");
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
generateCondensedPlan,
|
|
65
|
+
writeCondensedPlan,
|
|
66
|
+
deleteCondensedPlan
|
|
67
|
+
} = require("./condensed-plan");
|
|
68
|
+
|
|
69
|
+
const { ProgressTracker } = require("./progress-tracker");
|
|
70
|
+
|
|
71
|
+
const REPO_ROOT = process.cwd();
|
|
72
|
+
|
|
73
|
+
function parseArgs(argv) {
|
|
74
|
+
const options = {
|
|
75
|
+
plan: null,
|
|
76
|
+
dryRun: false,
|
|
77
|
+
verbose: false,
|
|
78
|
+
resume: false,
|
|
79
|
+
review: undefined
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < argv.length; i++) {
|
|
83
|
+
const arg = argv[i];
|
|
84
|
+
if (arg === "--plan") {
|
|
85
|
+
options.plan = argv[i + 1];
|
|
86
|
+
i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (arg.startsWith("--plan=")) {
|
|
90
|
+
options.plan = arg.split("=").slice(1).join("=");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (arg === "--dry-run") {
|
|
94
|
+
options.dryRun = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg === "--verbose") {
|
|
98
|
+
options.verbose = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (arg === "--resume") {
|
|
102
|
+
options.resume = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === "--review") {
|
|
106
|
+
options.review = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (arg.startsWith("--review=")) {
|
|
110
|
+
const value = arg.split("=").slice(1).join("=");
|
|
111
|
+
options.review = parseEnvBoolean(value);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return options;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseEnvBoolean(value) {
|
|
120
|
+
if (value === undefined || value === null) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const normalized = String(value).trim().toLowerCase();
|
|
125
|
+
if (["true", "1", "yes", "y", "on"].includes(normalized)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (["false", "0", "no", "n", "off"].includes(normalized)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseEnvInteger(value) {
|
|
136
|
+
if (value === undefined || value === null) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const parsed = parseInt(String(value).trim(), 10);
|
|
140
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveReviewEnabled(cliValue) {
|
|
144
|
+
if (typeof cliValue === "boolean") {
|
|
145
|
+
return cliValue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const envValue = parseEnvBoolean(process.env.ORRERY_REVIEW_ENABLED);
|
|
149
|
+
if (typeof envValue === "boolean") {
|
|
150
|
+
return envValue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return config.review.enabled;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveReviewMaxIterations(cliValue) {
|
|
157
|
+
if (Number.isFinite(cliValue) && cliValue > 0) {
|
|
158
|
+
return cliValue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const envValue = parseEnvInteger(process.env.ORRERY_REVIEW_MAX_ITERATIONS);
|
|
162
|
+
if (envValue !== undefined) {
|
|
163
|
+
return envValue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return config.review.maxIterations;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolvePlanFile(planArg, plansDir) {
|
|
170
|
+
if (!planArg) return null;
|
|
171
|
+
|
|
172
|
+
const candidates = [];
|
|
173
|
+
if (path.isAbsolute(planArg)) {
|
|
174
|
+
candidates.push(planArg);
|
|
175
|
+
} else {
|
|
176
|
+
candidates.push(path.resolve(process.cwd(), planArg));
|
|
177
|
+
candidates.push(path.join(plansDir, planArg));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const candidate of candidates) {
|
|
181
|
+
if (fs.existsSync(candidate)) {
|
|
182
|
+
return candidate;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function logDryRunSummary(planFiles) {
|
|
190
|
+
console.log("Dry run: no changes will be made.");
|
|
191
|
+
if (planFiles.length === 0) {
|
|
192
|
+
console.log("No plans to process.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(`Plans to process (${planFiles.length}):`);
|
|
197
|
+
for (const planFile of planFiles) {
|
|
198
|
+
const plan = loadPlan(planFile);
|
|
199
|
+
const totalSteps = plan.steps.length;
|
|
200
|
+
const completed = plan.steps.filter((s) => s.status === "complete").length;
|
|
201
|
+
const blocked = plan.steps.filter((s) => s.status === "blocked").length;
|
|
202
|
+
const pending = totalSteps - completed - blocked;
|
|
203
|
+
console.log(
|
|
204
|
+
` - ${path.basename(planFile)} (${pending} pending, ${completed} complete, ${blocked} blocked)`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
console.log();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Main orchestration function
|
|
212
|
+
*/
|
|
213
|
+
async function orchestrate(options = {}) {
|
|
214
|
+
const normalizedOptions = {
|
|
215
|
+
plan: options.plan || null,
|
|
216
|
+
dryRun: Boolean(options.dryRun),
|
|
217
|
+
verbose: Boolean(options.verbose),
|
|
218
|
+
resume: Boolean(options.resume),
|
|
219
|
+
review: options.review
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
config.logging.streamOutput = normalizedOptions.verbose;
|
|
223
|
+
config.review.enabled = resolveReviewEnabled(normalizedOptions.review);
|
|
224
|
+
|
|
225
|
+
console.log("=== Plan Orchestrator Starting ===\n");
|
|
226
|
+
|
|
227
|
+
const plansDir = getPlansDir();
|
|
228
|
+
const completedDir = getCompletedDir();
|
|
229
|
+
const reportsDir = getReportsDir();
|
|
230
|
+
|
|
231
|
+
// Record the source branch we're starting from
|
|
232
|
+
const sourceBranch = getCurrentBranch(REPO_ROOT);
|
|
233
|
+
console.log(`Source branch: ${sourceBranch}\n`);
|
|
234
|
+
|
|
235
|
+
// Check for uncommitted changes
|
|
236
|
+
if (hasUncommittedChanges(REPO_ROOT)) {
|
|
237
|
+
console.error(
|
|
238
|
+
"Error: Uncommitted changes detected. Please commit or stash before running orchestrator."
|
|
239
|
+
);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Resume mode: find and continue the plan for the current branch
|
|
244
|
+
if (normalizedOptions.resume) {
|
|
245
|
+
await handleResumeMode(plansDir, completedDir, reportsDir, sourceBranch);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Get list of completed plan filenames (to exclude)
|
|
250
|
+
const completedNames = getCompletedPlanNames(completedDir);
|
|
251
|
+
|
|
252
|
+
let planFiles = [];
|
|
253
|
+
let allPlanFiles = [];
|
|
254
|
+
|
|
255
|
+
if (normalizedOptions.plan) {
|
|
256
|
+
const resolvedPlanFile = resolvePlanFile(normalizedOptions.plan, plansDir);
|
|
257
|
+
if (!resolvedPlanFile) {
|
|
258
|
+
console.error(`Plan file not found: ${normalizedOptions.plan}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
if (completedNames.has(path.basename(resolvedPlanFile))) {
|
|
262
|
+
console.log(`Plan already completed: ${path.basename(resolvedPlanFile)}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
allPlanFiles = [resolvedPlanFile];
|
|
266
|
+
} else {
|
|
267
|
+
// Scan for active plans
|
|
268
|
+
allPlanFiles = getPlanFiles(plansDir).filter(
|
|
269
|
+
(f) => !completedNames.has(path.basename(f))
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Filter out plans that are already dispatched (have work_branch set)
|
|
274
|
+
const dispatchedPlans = [];
|
|
275
|
+
|
|
276
|
+
for (const planFile of allPlanFiles) {
|
|
277
|
+
const plan = loadPlan(planFile);
|
|
278
|
+
if (plan.metadata.work_branch) {
|
|
279
|
+
dispatchedPlans.push({
|
|
280
|
+
file: path.basename(planFile),
|
|
281
|
+
workBranch: plan.metadata.work_branch
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
planFiles.push(planFile);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (dispatchedPlans.length > 0) {
|
|
289
|
+
console.log(
|
|
290
|
+
`Skipping ${dispatchedPlans.length} already-dispatched plan(s):`
|
|
291
|
+
);
|
|
292
|
+
for (const dp of dispatchedPlans) {
|
|
293
|
+
console.log(` - ${dp.file} (work branch: ${dp.workBranch})`);
|
|
294
|
+
}
|
|
295
|
+
console.log();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (planFiles.length === 0) {
|
|
299
|
+
console.log(
|
|
300
|
+
`No new plans to process in ${path.relative(process.cwd(), plansDir)}/`
|
|
301
|
+
);
|
|
302
|
+
console.log(
|
|
303
|
+
"Create a plan file without work_branch metadata to get started."
|
|
304
|
+
);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (normalizedOptions.dryRun) {
|
|
309
|
+
logDryRunSummary(planFiles);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(`Found ${planFiles.length} plan(s) to process:\n`);
|
|
314
|
+
for (const pf of planFiles) {
|
|
315
|
+
console.log(` - ${path.basename(pf)}`);
|
|
316
|
+
}
|
|
317
|
+
console.log();
|
|
318
|
+
|
|
319
|
+
// Process each plan (one at a time, with branch switching)
|
|
320
|
+
for (const planFile of planFiles) {
|
|
321
|
+
await processPlanWithBranching(
|
|
322
|
+
planFile,
|
|
323
|
+
sourceBranch,
|
|
324
|
+
completedDir,
|
|
325
|
+
reportsDir
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Reload plan to check final state
|
|
329
|
+
const plan = loadPlan(planFile);
|
|
330
|
+
const isComplete = plan.isComplete();
|
|
331
|
+
const isSuccessful = plan.isSuccessful();
|
|
332
|
+
|
|
333
|
+
if (isComplete && isSuccessful) {
|
|
334
|
+
// Plan completed successfully - return to source branch for next plan
|
|
335
|
+
const currentBranch = getCurrentBranch(REPO_ROOT);
|
|
336
|
+
if (currentBranch !== sourceBranch) {
|
|
337
|
+
console.log(`\nReturning to source branch: ${sourceBranch}`);
|
|
338
|
+
checkoutBranch(sourceBranch, REPO_ROOT);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
// Plan is blocked - stay on work branch and stop processing
|
|
342
|
+
console.log(`\nPlan "${path.basename(planFile)}" is blocked.`);
|
|
343
|
+
console.log(`Staying on work branch: ${plan.metadata.work_branch}`);
|
|
344
|
+
console.log("\nTo continue:");
|
|
345
|
+
console.log(" 1. Fix the blocked steps (orrery status)");
|
|
346
|
+
console.log(" 2. Run 'orrery resume' to unblock and continue");
|
|
347
|
+
|
|
348
|
+
// List remaining unprocessed plans
|
|
349
|
+
const remaining = planFiles.slice(planFiles.indexOf(planFile) + 1);
|
|
350
|
+
if (remaining.length > 0) {
|
|
351
|
+
console.log(`\nSkipped ${remaining.length} remaining plan(s).`);
|
|
352
|
+
}
|
|
353
|
+
break; // Stop processing
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log("\n=== Orchestrator Complete ===");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Handle resume mode: find and continue plan for current branch
|
|
362
|
+
*/
|
|
363
|
+
async function handleResumeMode(
|
|
364
|
+
plansDir,
|
|
365
|
+
completedDir,
|
|
366
|
+
reportsDir,
|
|
367
|
+
currentBranch
|
|
368
|
+
) {
|
|
369
|
+
console.log("=== Resume Mode ===\n");
|
|
370
|
+
console.log(`Looking for plan with work_branch: ${currentBranch}\n`);
|
|
371
|
+
|
|
372
|
+
// Get all plan files (including dispatched ones)
|
|
373
|
+
const completedNames = getCompletedPlanNames(completedDir);
|
|
374
|
+
const allPlanFiles = getPlanFiles(plansDir).filter(
|
|
375
|
+
(f) => !completedNames.has(path.basename(f))
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Find plan matching current branch
|
|
379
|
+
let matchingPlanFile = null;
|
|
380
|
+
let matchingPlan = null;
|
|
381
|
+
|
|
382
|
+
for (const planFile of allPlanFiles) {
|
|
383
|
+
const plan = loadPlan(planFile);
|
|
384
|
+
if (plan.metadata.work_branch === currentBranch) {
|
|
385
|
+
matchingPlanFile = planFile;
|
|
386
|
+
matchingPlan = plan;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!matchingPlanFile) {
|
|
392
|
+
console.error(`No plan found with work_branch matching "${currentBranch}"`);
|
|
393
|
+
console.log("\nTo resume a plan:");
|
|
394
|
+
console.log(" 1. git checkout <work-branch>");
|
|
395
|
+
console.log(" 2. orrery exec --resume");
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const planFileName = path.basename(matchingPlanFile);
|
|
400
|
+
console.log(`Found plan: ${planFileName}`);
|
|
401
|
+
|
|
402
|
+
// Check if plan has pending steps
|
|
403
|
+
if (matchingPlan.isComplete()) {
|
|
404
|
+
console.log("\nPlan is already complete (no pending steps).");
|
|
405
|
+
console.log("Use normal mode to create a PR or archive the plan.");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const pendingSteps = matchingPlan.steps.filter((s) => s.status === "pending");
|
|
410
|
+
const inProgressSteps = matchingPlan.steps.filter(
|
|
411
|
+
(s) => s.status === "in_progress"
|
|
412
|
+
);
|
|
413
|
+
console.log(`Pending steps: ${pendingSteps.length}`);
|
|
414
|
+
if (inProgressSteps.length > 0) {
|
|
415
|
+
console.log(
|
|
416
|
+
`In-progress steps (will be retried): ${inProgressSteps.length}`
|
|
417
|
+
);
|
|
418
|
+
// Reset in_progress steps to pending so they get retried
|
|
419
|
+
for (const step of inProgressSteps) {
|
|
420
|
+
step.status = "pending";
|
|
421
|
+
}
|
|
422
|
+
savePlan(matchingPlan);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
console.log("\nResuming plan execution...\n");
|
|
426
|
+
|
|
427
|
+
// Process the plan (reuse existing processPlan logic)
|
|
428
|
+
await processPlan(matchingPlanFile, completedDir, reportsDir);
|
|
429
|
+
|
|
430
|
+
// Reload and check final state
|
|
431
|
+
matchingPlan = loadPlan(matchingPlanFile);
|
|
432
|
+
const isComplete = matchingPlan.isComplete();
|
|
433
|
+
|
|
434
|
+
if (isComplete) {
|
|
435
|
+
// Archive and create PR
|
|
436
|
+
archivePlan(matchingPlanFile, matchingPlan, completedDir);
|
|
437
|
+
|
|
438
|
+
const workCommit = commit(
|
|
439
|
+
`chore: complete plan ${planFileName}`,
|
|
440
|
+
[],
|
|
441
|
+
REPO_ROOT
|
|
442
|
+
);
|
|
443
|
+
if (workCommit) {
|
|
444
|
+
console.log(`Committed plan completion (${workCommit.slice(0, 7)})`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const sourceBranch = matchingPlan.metadata.source_branch || "main";
|
|
448
|
+
const prTitle = `Plan: ${planFileName.replace(/\.ya?ml$/, "").replace(/^\d{4}-\d{2}-\d{2}-/, "")}`;
|
|
449
|
+
const prBody = generatePRBody(matchingPlan);
|
|
450
|
+
const prInfo = createPullRequest(prTitle, prBody, sourceBranch, REPO_ROOT);
|
|
451
|
+
logPullRequestInfo(prInfo);
|
|
452
|
+
} else {
|
|
453
|
+
const progressCommit = commit(
|
|
454
|
+
`wip: progress on plan ${planFileName}`,
|
|
455
|
+
[],
|
|
456
|
+
REPO_ROOT
|
|
457
|
+
);
|
|
458
|
+
if (progressCommit) {
|
|
459
|
+
console.log(`Committed work-in-progress (${progressCommit.slice(0, 7)})`);
|
|
460
|
+
}
|
|
461
|
+
console.log(
|
|
462
|
+
"\nPlan still has pending steps. Run --resume again to continue."
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log("\n=== Resume Complete ===");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Process a single plan with branch management
|
|
471
|
+
*/
|
|
472
|
+
async function processPlanWithBranching(
|
|
473
|
+
planFile,
|
|
474
|
+
sourceBranch,
|
|
475
|
+
completedDir,
|
|
476
|
+
reportsDir
|
|
477
|
+
) {
|
|
478
|
+
const planFileName = path.basename(planFile);
|
|
479
|
+
console.log(`\n--- Processing: ${planFileName} ---\n`);
|
|
480
|
+
|
|
481
|
+
// Step 1: Determine work branch name
|
|
482
|
+
const workBranch = deriveBranchName(planFileName);
|
|
483
|
+
console.log(`Work branch: ${workBranch}`);
|
|
484
|
+
|
|
485
|
+
// Step 2: Update plan metadata on source branch to mark as dispatched
|
|
486
|
+
let plan = loadPlan(planFile);
|
|
487
|
+
plan.metadata.source_branch = sourceBranch;
|
|
488
|
+
plan.metadata.work_branch = workBranch;
|
|
489
|
+
savePlan(plan);
|
|
490
|
+
|
|
491
|
+
// Commit the metadata update on source branch
|
|
492
|
+
const metadataCommit = commit(
|
|
493
|
+
`chore: dispatch plan ${planFileName} to ${workBranch}`,
|
|
494
|
+
[planFile],
|
|
495
|
+
REPO_ROOT
|
|
496
|
+
);
|
|
497
|
+
if (metadataCommit) {
|
|
498
|
+
console.log(
|
|
499
|
+
`Marked plan as dispatched on ${sourceBranch} (${metadataCommit.slice(0, 7)})`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Step 3: Create and switch to work branch
|
|
504
|
+
if (branchExists(workBranch, REPO_ROOT)) {
|
|
505
|
+
console.log(`Work branch ${workBranch} already exists, checking out...`);
|
|
506
|
+
checkoutBranch(workBranch, REPO_ROOT);
|
|
507
|
+
} else {
|
|
508
|
+
console.log(`Creating work branch: ${workBranch}`);
|
|
509
|
+
createBranch(workBranch, REPO_ROOT);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Step 4: Process the plan (main execution logic)
|
|
513
|
+
await processPlan(planFile, completedDir, reportsDir);
|
|
514
|
+
|
|
515
|
+
// Step 5: Reload plan to check final state
|
|
516
|
+
plan = loadPlan(planFile);
|
|
517
|
+
const isComplete = plan.isComplete();
|
|
518
|
+
|
|
519
|
+
if (isComplete) {
|
|
520
|
+
// Step 6: Archive the plan (on work branch)
|
|
521
|
+
archivePlan(planFile, plan, completedDir);
|
|
522
|
+
|
|
523
|
+
// Step 7: Commit all work branch changes
|
|
524
|
+
const workCommit = commit(
|
|
525
|
+
`chore: complete plan ${planFileName}`,
|
|
526
|
+
[], // Stage all changes
|
|
527
|
+
REPO_ROOT
|
|
528
|
+
);
|
|
529
|
+
if (workCommit) {
|
|
530
|
+
console.log(`Committed plan completion (${workCommit.slice(0, 7)})`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Step 8: Generate PR info
|
|
534
|
+
const prTitle = `Plan: ${planFileName.replace(/\.ya?ml$/, "").replace(/^\d{4}-\d{2}-\d{2}-/, "")}`;
|
|
535
|
+
const prBody = generatePRBody(plan);
|
|
536
|
+
const prInfo = createPullRequest(prTitle, prBody, sourceBranch, REPO_ROOT);
|
|
537
|
+
logPullRequestInfo(prInfo);
|
|
538
|
+
} else {
|
|
539
|
+
// Plan not complete (still has pending steps or was interrupted)
|
|
540
|
+
// Commit any progress made
|
|
541
|
+
const progressCommit = commit(
|
|
542
|
+
`wip: progress on plan ${planFileName}`,
|
|
543
|
+
[],
|
|
544
|
+
REPO_ROOT
|
|
545
|
+
);
|
|
546
|
+
if (progressCommit) {
|
|
547
|
+
console.log(`Committed work-in-progress (${progressCommit.slice(0, 7)})`);
|
|
548
|
+
}
|
|
549
|
+
console.log(
|
|
550
|
+
"\nPlan not complete. Work branch preserved for later continuation."
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Log pull request information for user to create PR manually
|
|
557
|
+
* @param {{url: string, title: string, body: string, headBranch: string, baseBranch: string, pushed: boolean}} prInfo
|
|
558
|
+
*/
|
|
559
|
+
function logPullRequestInfo(prInfo) {
|
|
560
|
+
console.log("\n=== Pull Request Ready ===\n");
|
|
561
|
+
|
|
562
|
+
if (prInfo.pushed) {
|
|
563
|
+
console.log(`Branch pushed: ${prInfo.headBranch} -> origin`);
|
|
564
|
+
} else {
|
|
565
|
+
console.log(
|
|
566
|
+
`Note: Could not push branch. Run: git push -u origin ${prInfo.headBranch}`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
console.log(`\nBase branch: ${prInfo.baseBranch}`);
|
|
571
|
+
console.log(`Head branch: ${prInfo.headBranch}`);
|
|
572
|
+
|
|
573
|
+
if (prInfo.url) {
|
|
574
|
+
console.log(`\nCreate PR: ${prInfo.url}`);
|
|
575
|
+
} else {
|
|
576
|
+
console.log("\nCould not generate PR URL (no remote configured).");
|
|
577
|
+
console.log("Create the PR manually on your Git hosting platform.");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.log("\n--- PR Title ---");
|
|
581
|
+
console.log(prInfo.title);
|
|
582
|
+
console.log("\n--- PR Body ---");
|
|
583
|
+
console.log(prInfo.body);
|
|
584
|
+
console.log("----------------\n");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Generate PR body from completed plan
|
|
589
|
+
*/
|
|
590
|
+
function generatePRBody(plan) {
|
|
591
|
+
const steps = plan.steps || [];
|
|
592
|
+
const completed = steps.filter((s) => s.status === "complete").length;
|
|
593
|
+
const blocked = steps.filter((s) => s.status === "blocked").length;
|
|
594
|
+
const total = steps.length;
|
|
595
|
+
|
|
596
|
+
let body = `## Plan Summary\n\n`;
|
|
597
|
+
body += `- **Status:** ${plan.metadata.outcome === "success" ? "All steps complete" : "Partial (some steps blocked)"}\n`;
|
|
598
|
+
body += `- **Steps:** ${completed}/${total} complete`;
|
|
599
|
+
if (blocked > 0) {
|
|
600
|
+
body += `, ${blocked} blocked`;
|
|
601
|
+
}
|
|
602
|
+
body += `\n\n`;
|
|
603
|
+
|
|
604
|
+
body += `## Steps\n\n`;
|
|
605
|
+
for (const step of steps) {
|
|
606
|
+
const icon =
|
|
607
|
+
step.status === "complete" ? "x" : step.status === "blocked" ? "-" : " ";
|
|
608
|
+
body += `- [${icon}] **${step.id}**: ${step.description}\n`;
|
|
609
|
+
if (step.status === "blocked" && step.blocked_reason) {
|
|
610
|
+
body += ` - Blocked: ${step.blocked_reason}\n`;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
body += `\n---\n*Generated by Orrery*`;
|
|
615
|
+
return body;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Process a single plan file (core execution logic)
|
|
620
|
+
*/
|
|
621
|
+
async function processPlan(planFile, completedDir, reportsDir) {
|
|
622
|
+
let plan = loadPlan(planFile);
|
|
623
|
+
const activeAgents = []; // Array of {handle, stepIds}
|
|
624
|
+
|
|
625
|
+
// Initialize progress tracker
|
|
626
|
+
const tracker = new ProgressTracker(
|
|
627
|
+
plan.steps.length,
|
|
628
|
+
path.basename(planFile)
|
|
629
|
+
);
|
|
630
|
+
tracker.initializeFromPlan(plan);
|
|
631
|
+
tracker.logStart();
|
|
632
|
+
|
|
633
|
+
// Check initial state
|
|
634
|
+
if (plan.isComplete()) {
|
|
635
|
+
console.log("Plan is already complete.");
|
|
636
|
+
tracker.logSummary();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Main execution loop
|
|
641
|
+
while (!plan.isComplete()) {
|
|
642
|
+
// Get steps ready to execute
|
|
643
|
+
const readySteps = getReadySteps(plan);
|
|
644
|
+
|
|
645
|
+
if (readySteps.length === 0) {
|
|
646
|
+
// Check if we have running agents
|
|
647
|
+
if (activeAgents.length > 0) {
|
|
648
|
+
// Wait for at least one to complete
|
|
649
|
+
const { stepIds, parsedResults } = await waitForAgentCompletion(
|
|
650
|
+
planFile,
|
|
651
|
+
activeAgents,
|
|
652
|
+
reportsDir,
|
|
653
|
+
tracker
|
|
654
|
+
);
|
|
655
|
+
plan = loadPlan(planFile); // Reload to get status updates
|
|
656
|
+
|
|
657
|
+
// Commit agent work using their commit message
|
|
658
|
+
if (hasUncommittedChanges(REPO_ROOT)) {
|
|
659
|
+
const commitMsg =
|
|
660
|
+
parsedResults[0]?.commitMessage ||
|
|
661
|
+
`feat: complete step(s) ${stepIds.join(", ")}`;
|
|
662
|
+
const commitSha = commit(commitMsg, [], REPO_ROOT);
|
|
663
|
+
if (commitSha) {
|
|
664
|
+
console.log(
|
|
665
|
+
`Committed: ${commitMsg.split("\n")[0]} (${commitSha.slice(0, 7)})`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
continue;
|
|
670
|
+
} else {
|
|
671
|
+
// No ready steps and no running agents = fully blocked
|
|
672
|
+
console.log("Plan is blocked - no executable steps remaining.");
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Determine how many we can start
|
|
678
|
+
const currentlyRunning = activeAgents.reduce(
|
|
679
|
+
(sum, a) => sum + a.stepIds.length,
|
|
680
|
+
0
|
|
681
|
+
);
|
|
682
|
+
const { parallel, serial } = partitionSteps(
|
|
683
|
+
readySteps,
|
|
684
|
+
config.concurrency.maxParallel,
|
|
685
|
+
currentlyRunning
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
// Start parallel steps together (batch invocation)
|
|
689
|
+
if (parallel.length > 0) {
|
|
690
|
+
const stepIds = parallel.map((s) => s.id);
|
|
691
|
+
await startSteps(planFile, stepIds, activeAgents, tracker);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Start serial steps individually
|
|
695
|
+
for (const step of serial) {
|
|
696
|
+
if (
|
|
697
|
+
activeAgents.reduce((sum, a) => sum + a.stepIds.length, 0) >=
|
|
698
|
+
config.concurrency.maxParallel
|
|
699
|
+
) {
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
await startSteps(planFile, [step.id], activeAgents, tracker);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// If we started any agents, wait for at least one to complete
|
|
706
|
+
if (activeAgents.length > 0) {
|
|
707
|
+
const { stepIds, parsedResults } = await waitForAgentCompletion(
|
|
708
|
+
planFile,
|
|
709
|
+
activeAgents,
|
|
710
|
+
reportsDir,
|
|
711
|
+
tracker
|
|
712
|
+
);
|
|
713
|
+
plan = loadPlan(planFile); // Reload to get status updates
|
|
714
|
+
|
|
715
|
+
// Commit agent work using their commit message
|
|
716
|
+
if (hasUncommittedChanges(REPO_ROOT)) {
|
|
717
|
+
const commitMsg =
|
|
718
|
+
parsedResults[0]?.commitMessage ||
|
|
719
|
+
`feat: complete step(s) ${stepIds.join(", ")}`;
|
|
720
|
+
const commitSha = commit(commitMsg, [], REPO_ROOT);
|
|
721
|
+
if (commitSha) {
|
|
722
|
+
console.log(
|
|
723
|
+
`Committed: ${commitMsg.split("\n")[0]} (${commitSha.slice(0, 7)})`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Log final summary
|
|
731
|
+
tracker.logSummary();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Start steps by marking them in_progress and invoking an agent
|
|
736
|
+
* @param {string} planFile - Path to the plan file
|
|
737
|
+
* @param {string[]} stepIds - Array of step IDs to start
|
|
738
|
+
* @param {Object[]} activeAgents - Array of active agent handles
|
|
739
|
+
* @param {ProgressTracker} tracker - Progress tracker instance
|
|
740
|
+
*/
|
|
741
|
+
async function startSteps(planFile, stepIds, activeAgents, tracker) {
|
|
742
|
+
// Log step start with progress info
|
|
743
|
+
tracker.logStepStart(stepIds);
|
|
744
|
+
|
|
745
|
+
// Mark steps as in_progress
|
|
746
|
+
const updates = stepIds.map((stepId) => ({
|
|
747
|
+
stepId,
|
|
748
|
+
status: "in_progress"
|
|
749
|
+
}));
|
|
750
|
+
updateStepsStatus(planFile, updates);
|
|
751
|
+
|
|
752
|
+
// Generate condensed plan with only assigned steps and their completed dependencies
|
|
753
|
+
const plan = loadPlan(planFile);
|
|
754
|
+
const condensedPlan = generateCondensedPlan(plan, stepIds);
|
|
755
|
+
const tempPlanFile = writeCondensedPlan(condensedPlan, planFile, stepIds);
|
|
756
|
+
|
|
757
|
+
// Invoke the agent with the condensed plan
|
|
758
|
+
const handle = invokeAgentWithFailover(
|
|
759
|
+
config,
|
|
760
|
+
tempPlanFile,
|
|
761
|
+
stepIds,
|
|
762
|
+
REPO_ROOT,
|
|
763
|
+
{
|
|
764
|
+
onStdout: (text, ids) => {
|
|
765
|
+
if (config.logging.streamOutput) {
|
|
766
|
+
const prefix = `[${ids.join(",")}]`;
|
|
767
|
+
const lines = text.trim().split("\n");
|
|
768
|
+
for (const line of lines) {
|
|
769
|
+
if (line.trim()) {
|
|
770
|
+
console.log(`${prefix} ${line}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
onStderr: (text, ids) => {
|
|
776
|
+
if (config.logging.streamOutput) {
|
|
777
|
+
const prefix = `[${ids.join(",")}]`;
|
|
778
|
+
const lines = text.trim().split("\n");
|
|
779
|
+
for (const line of lines) {
|
|
780
|
+
if (line.trim()) {
|
|
781
|
+
// Note: stderrIsProgress is handled per-agent inside failover
|
|
782
|
+
console.log(`${prefix} ${line}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
activeAgents.push({ handle, stepIds, tempPlanFile });
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Wait for any agent to complete and process its results
|
|
795
|
+
* @param {string} planFile - Path to the plan file
|
|
796
|
+
* @param {Object[]} activeAgents - Array of active agent handles
|
|
797
|
+
* @param {string} reportsDir - Directory for reports
|
|
798
|
+
* @param {ProgressTracker} tracker - Progress tracker instance
|
|
799
|
+
* @returns {{stepIds: string[], parsedResults: Object[]}} Completed step IDs and parsed results
|
|
800
|
+
*/
|
|
801
|
+
async function waitForAgentCompletion(
|
|
802
|
+
planFile,
|
|
803
|
+
activeAgents,
|
|
804
|
+
reportsDir,
|
|
805
|
+
tracker
|
|
806
|
+
) {
|
|
807
|
+
if (activeAgents.length === 0) return { stepIds: [], parsedResults: [] };
|
|
808
|
+
|
|
809
|
+
// Wait for any agent to complete
|
|
810
|
+
const { result, index } = await waitForAny(activeAgents.map((a) => a.handle));
|
|
811
|
+
const { stepIds, tempPlanFile } = activeAgents[index];
|
|
812
|
+
|
|
813
|
+
// Remove completed agent from active list
|
|
814
|
+
activeAgents.splice(index, 1);
|
|
815
|
+
|
|
816
|
+
const agentName = result.agentName || "unknown";
|
|
817
|
+
console.log(
|
|
818
|
+
`Agent ${agentName} for step(s) ${stepIds.join(", ")} exited with code ${result.exitCode}`
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// Log failure details for debugging
|
|
822
|
+
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
823
|
+
console.log(`[${agentName}] FAILED (exit ${result.exitCode})`);
|
|
824
|
+
if (result.stderr) {
|
|
825
|
+
console.log(`[${agentName}] stderr:\n${result.stderr}`);
|
|
826
|
+
}
|
|
827
|
+
if (result.stdout) {
|
|
828
|
+
console.log(`[${agentName}] stdout:\n${result.stdout}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Parse results from stdout
|
|
833
|
+
let parsedResults = parseAgentResults(result.stdout);
|
|
834
|
+
|
|
835
|
+
const planForReview = loadPlan(planFile);
|
|
836
|
+
|
|
837
|
+
// Build updates for each step
|
|
838
|
+
const updates = [];
|
|
839
|
+
const reports = [];
|
|
840
|
+
|
|
841
|
+
for (const stepId of stepIds) {
|
|
842
|
+
// Find parsed result for this step, or create default
|
|
843
|
+
let stepResult = parsedResults.find((r) => r.stepId === stepId);
|
|
844
|
+
if (!stepResult) {
|
|
845
|
+
console.log(`[DEBUG] No report found for step ${stepId}`);
|
|
846
|
+
if (!result.stdout || result.stdout.trim().length === 0) {
|
|
847
|
+
console.log(`[DEBUG] Agent stdout was empty`);
|
|
848
|
+
} else {
|
|
849
|
+
console.log(`[DEBUG] Agent stdout:\n${result.stdout}`);
|
|
850
|
+
console.log(`[DEBUG] Parsed results:`, parsedResults);
|
|
851
|
+
}
|
|
852
|
+
stepResult = createDefaultResult(stepId, result.exitCode, result.stderr);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (config.review.enabled && stepResult.status === "complete") {
|
|
856
|
+
const maxIterations = resolveReviewMaxIterations();
|
|
857
|
+
if (maxIterations > 0) {
|
|
858
|
+
const stepData =
|
|
859
|
+
(planForReview.steps || []).find((step) => step.id === stepId) ||
|
|
860
|
+
null;
|
|
861
|
+
const stepContext = stepData
|
|
862
|
+
? {
|
|
863
|
+
id: stepData.id,
|
|
864
|
+
description: stepData.description,
|
|
865
|
+
context: stepData.context,
|
|
866
|
+
requirements: stepData.requirements,
|
|
867
|
+
criteria: stepData.criteria,
|
|
868
|
+
files: stepData.files,
|
|
869
|
+
risk_notes: stepData.risk_notes
|
|
870
|
+
}
|
|
871
|
+
: `Step ${stepId} context not found.`;
|
|
872
|
+
|
|
873
|
+
let approved = false;
|
|
874
|
+
let currentResult = stepResult;
|
|
875
|
+
|
|
876
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
877
|
+
const files =
|
|
878
|
+
Array.isArray(currentResult.artifacts) &&
|
|
879
|
+
currentResult.artifacts.length > 0
|
|
880
|
+
? currentResult.artifacts
|
|
881
|
+
: stepData && Array.isArray(stepData.files)
|
|
882
|
+
? stepData.files
|
|
883
|
+
: [];
|
|
884
|
+
const diff = getUncommittedDiff(REPO_ROOT, files);
|
|
885
|
+
|
|
886
|
+
console.log(
|
|
887
|
+
`Review iteration ${iteration}/${maxIterations} for step ${stepId}`
|
|
888
|
+
);
|
|
889
|
+
const reviewResult = await invokeReviewAgent(
|
|
890
|
+
config,
|
|
891
|
+
stepContext,
|
|
892
|
+
files,
|
|
893
|
+
diff,
|
|
894
|
+
REPO_ROOT,
|
|
895
|
+
{
|
|
896
|
+
planFile,
|
|
897
|
+
stepId,
|
|
898
|
+
stepIds: [stepId]
|
|
899
|
+
}
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
if (reviewResult.error) {
|
|
903
|
+
console.log(
|
|
904
|
+
`[WARN] Review output parse issue for step ${stepId}: ${reviewResult.error}`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (reviewResult.approved) {
|
|
909
|
+
console.log(`Review approved for step ${stepId}`);
|
|
910
|
+
approved = true;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const issueCount = reviewResult.feedback.length;
|
|
915
|
+
console.log(
|
|
916
|
+
`Review needs changes for step ${stepId}: ${issueCount} issue(s)`
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
if (iteration >= maxIterations) {
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const editResults = await invokeEditAgent(
|
|
924
|
+
config,
|
|
925
|
+
planFile,
|
|
926
|
+
[stepId],
|
|
927
|
+
reviewResult.feedback,
|
|
928
|
+
REPO_ROOT,
|
|
929
|
+
{
|
|
930
|
+
stepId,
|
|
931
|
+
stepIds: [stepId]
|
|
932
|
+
}
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
const editedResult =
|
|
936
|
+
editResults.find((r) => r.stepId === stepId) ||
|
|
937
|
+
createDefaultResult(stepId, null, "Edit agent returned no report");
|
|
938
|
+
currentResult = editedResult;
|
|
939
|
+
|
|
940
|
+
if (currentResult.status !== "complete") {
|
|
941
|
+
console.log(
|
|
942
|
+
`Edit agent reported ${currentResult.status} for step ${stepId}`
|
|
943
|
+
);
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (!approved && currentResult.status === "complete") {
|
|
949
|
+
console.log(
|
|
950
|
+
`[WARN] Review max iterations reached for step ${stepId}. Proceeding without approval.`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
stepResult = currentResult;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
parsedResults = parsedResults.filter((r) => r.stepId !== stepId);
|
|
959
|
+
parsedResults.push(stepResult);
|
|
960
|
+
|
|
961
|
+
// Prepare plan update (include agent name)
|
|
962
|
+
const update = {
|
|
963
|
+
stepId,
|
|
964
|
+
status: stepResult.status,
|
|
965
|
+
extras: { agent: agentName }
|
|
966
|
+
};
|
|
967
|
+
if (stepResult.status === "blocked" && stepResult.blockedReason) {
|
|
968
|
+
update.extras.blocked_reason = stepResult.blockedReason;
|
|
969
|
+
}
|
|
970
|
+
updates.push(update);
|
|
971
|
+
|
|
972
|
+
// Prepare report
|
|
973
|
+
reports.push({
|
|
974
|
+
step_id: stepId,
|
|
975
|
+
agent: agentName,
|
|
976
|
+
outcome: stepResult.status === "complete" ? "success" : "failure",
|
|
977
|
+
details: stepResult.summary || "",
|
|
978
|
+
timestamp: new Date().toISOString(),
|
|
979
|
+
artifacts: stepResult.artifacts || [],
|
|
980
|
+
blocked_reason: stepResult.blockedReason || null,
|
|
981
|
+
test_results: stepResult.testResults || null
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Update plan file
|
|
986
|
+
updateStepsStatus(planFile, updates);
|
|
987
|
+
|
|
988
|
+
// Write reports
|
|
989
|
+
for (const report of reports) {
|
|
990
|
+
writeReport(reportsDir, planFile, report);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Update progress tracker
|
|
994
|
+
for (const update of updates) {
|
|
995
|
+
if (update.status === "complete") {
|
|
996
|
+
tracker.recordComplete(update.stepId);
|
|
997
|
+
} else if (update.status === "blocked") {
|
|
998
|
+
tracker.recordBlocked(update.stepId);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
tracker.logProgress();
|
|
1002
|
+
|
|
1003
|
+
// Handle blocked step cascades
|
|
1004
|
+
const plan = loadPlan(planFile);
|
|
1005
|
+
for (const update of updates) {
|
|
1006
|
+
if (update.status === "blocked") {
|
|
1007
|
+
const dependents = getBlockedDependents(plan, update.stepId);
|
|
1008
|
+
if (dependents.length > 0) {
|
|
1009
|
+
console.log(
|
|
1010
|
+
`Step ${update.stepId} blocked. Dependent steps affected: ${dependents.join(", ")}`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Clean up temp plan file
|
|
1017
|
+
if (tempPlanFile) {
|
|
1018
|
+
deleteCondensedPlan(tempPlanFile);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return { stepIds, parsedResults };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Write a step report to the reports directory
|
|
1026
|
+
*/
|
|
1027
|
+
function writeReport(reportsDir, planFile, report) {
|
|
1028
|
+
const planName = path.basename(planFile, ".yaml");
|
|
1029
|
+
const fileName = `${planName}-${report.step_id}-report.yaml`;
|
|
1030
|
+
const filePath = path.join(reportsDir, fileName);
|
|
1031
|
+
|
|
1032
|
+
const content = YAML.stringify(report);
|
|
1033
|
+
|
|
1034
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
1035
|
+
console.log(`Report written: ${fileName}`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Archive a completed plan
|
|
1040
|
+
*/
|
|
1041
|
+
function archivePlan(planFile, plan, completedDir) {
|
|
1042
|
+
const success = plan.isSuccessful();
|
|
1043
|
+
const status = success ? "SUCCESS" : "PARTIAL (some steps blocked)";
|
|
1044
|
+
|
|
1045
|
+
// Add completion metadata
|
|
1046
|
+
plan.metadata.completed_at = new Date().toISOString();
|
|
1047
|
+
plan.metadata.outcome = success ? "success" : "partial";
|
|
1048
|
+
savePlan(plan);
|
|
1049
|
+
|
|
1050
|
+
// Move to completed
|
|
1051
|
+
const destPath = movePlanToCompleted(planFile, completedDir);
|
|
1052
|
+
console.log(`\nPlan archived: ${status}`);
|
|
1053
|
+
console.log(` -> ${path.relative(REPO_ROOT, destPath)}`);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Run if called directly
|
|
1057
|
+
if (require.main === module) {
|
|
1058
|
+
const cliOptions = parseArgs(process.argv.slice(2));
|
|
1059
|
+
orchestrate(cliOptions).catch((err) => {
|
|
1060
|
+
console.error("Orchestrator error:", err);
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
module.exports = { orchestrate };
|