codex-orchestrator 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -13
- package/dist/src/config/schema.d.ts +7 -0
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +34 -0
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/git/worktree.d.ts.map +1 -1
- package/dist/src/git/worktree.js +1 -6
- package/dist/src/git/worktree.js.map +1 -1
- package/dist/src/path-policy.d.ts +27 -0
- package/dist/src/path-policy.d.ts.map +1 -0
- package/dist/src/path-policy.js +83 -0
- package/dist/src/path-policy.js.map +1 -0
- package/dist/src/runner/command-utils.d.ts +3 -2
- package/dist/src/runner/command-utils.d.ts.map +1 -1
- package/dist/src/runner/command-utils.js +71 -4
- package/dist/src/runner/command-utils.js.map +1 -1
- package/dist/src/runner/handoff-evidence.d.ts +105 -0
- package/dist/src/runner/handoff-evidence.d.ts.map +1 -0
- package/dist/src/runner/handoff-evidence.js +224 -0
- package/dist/src/runner/handoff-evidence.js.map +1 -0
- package/dist/src/runner/issue-tree.d.ts +4 -1
- package/dist/src/runner/issue-tree.d.ts.map +1 -1
- package/dist/src/runner/issue-tree.js +75 -0
- package/dist/src/runner/issue-tree.js.map +1 -1
- package/dist/src/runner/local-execution-session.d.ts +45 -0
- package/dist/src/runner/local-execution-session.d.ts.map +1 -1
- package/dist/src/runner/local-execution-session.js +229 -0
- package/dist/src/runner/local-execution-session.js.map +1 -1
- package/dist/src/runner/plan-auto-command.d.ts.map +1 -1
- package/dist/src/runner/plan-auto-command.js +122 -307
- package/dist/src/runner/plan-auto-command.js.map +1 -1
- package/dist/src/runner/prompt.d.ts +4 -0
- package/dist/src/runner/prompt.d.ts.map +1 -1
- package/dist/src/runner/prompt.js +15 -45
- package/dist/src/runner/prompt.js.map +1 -1
- package/dist/src/runner/review-gate-policy.d.ts +24 -0
- package/dist/src/runner/review-gate-policy.d.ts.map +1 -0
- package/dist/src/runner/review-gate-policy.js +105 -0
- package/dist/src/runner/review-gate-policy.js.map +1 -0
- package/dist/src/runner/review-gates.d.ts +4 -8
- package/dist/src/runner/review-gates.d.ts.map +1 -1
- package/dist/src/runner/review-gates.js +27 -101
- package/dist/src/runner/review-gates.js.map +1 -1
- package/dist/src/runner/safety.d.ts.map +1 -1
- package/dist/src/runner/safety.js +5 -20
- package/dist/src/runner/safety.js.map +1 -1
- package/dist/src/runner/scoped-auto-command.d.ts.map +1 -1
- package/dist/src/runner/scoped-auto-command.js +137 -288
- package/dist/src/runner/scoped-auto-command.js.map +1 -1
- package/dist/src/runner/visual-proof-runner.d.ts +2 -7
- package/dist/src/runner/visual-proof-runner.d.ts.map +1 -1
- package/dist/src/runner/visual-proof-runner.js +13 -14
- package/dist/src/runner/visual-proof-runner.js.map +1 -1
- package/dist/src/setup/project-config.d.ts.map +1 -1
- package/dist/src/setup/project-config.js +19 -5
- package/dist/src/setup/project-config.js.map +1 -1
- package/dist/src/setup/setup-command.d.ts.map +1 -1
- package/dist/src/setup/setup-command.js +42 -0
- package/dist/src/setup/setup-command.js.map +1 -1
- package/package.json +1 -1
- package/prompts/workflows/scoped-implementation.md +3 -3
|
@@ -5,17 +5,16 @@ import { GitMergeConflictError, GitWorktreeManager, renderBranchTemplate } from
|
|
|
5
5
|
import { GhCliIssueAdapter } from '../github/gh-issue-adapter.js';
|
|
6
6
|
import { GhCliPullRequestAdapter } from '../github/gh-pull-request-adapter.js';
|
|
7
7
|
import { defaultShellCommandExecutor } from '../process/command.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { formatSessionTimestamp, readRunnerConfig, runConfiguredChecks, } from './command-utils.js';
|
|
9
|
+
import { buildChildBlockedReport, buildChildReviewReport, buildIssueTreePullRequestBody, buildIssueTreeReviewReport, buildParentBlockedReport, } from './handoff-evidence.js';
|
|
10
|
+
import { readPlanAutoCompletionReport, } from './completion-report.js';
|
|
10
11
|
import { claimIssue, discoverIssueWork } from './issue-state-machine.js';
|
|
11
|
-
import { collectExecutableChildBatches,
|
|
12
|
+
import { collectExecutableChildBatches, persistAutonomousChildNode, readAutonomousChildNodes, topologicalPlanNodes, } from './issue-tree.js';
|
|
13
|
+
import { runImplementationPublishabilityCheck } from './local-execution-session.js';
|
|
12
14
|
import { RunnerStateStore } from './local-state.js';
|
|
13
15
|
import { buildIssueTreeChildPrompt, buildPlanAutoPrompt, sessionPromptPath, sessionReportPath, writeDurablePrompt, } from './prompt.js';
|
|
14
|
-
import { evaluateReviewGates } from './review-gates.js';
|
|
15
16
|
import { sessionLogPath } from './run-log.js';
|
|
16
|
-
import { validateChangedPaths, validateCompletionReportSafety, validateNoAgentOwnedGitPublication, } from './safety.js';
|
|
17
17
|
import { cleanupSessionCodexHome, sessionCodexHomePath } from './session-home.js';
|
|
18
|
-
import { runRunnerVisualProof } from './visual-proof-runner.js';
|
|
19
18
|
export async function runPlanAutoCommand(options) {
|
|
20
19
|
const targetRoot = resolve(options.targetRoot);
|
|
21
20
|
const now = options.now ?? new Date();
|
|
@@ -43,6 +42,8 @@ export async function runPlanAutoCommand(options) {
|
|
|
43
42
|
let logPath = '';
|
|
44
43
|
const childIssues = [];
|
|
45
44
|
let pullRequest;
|
|
45
|
+
let store;
|
|
46
|
+
const executionResults = [];
|
|
46
47
|
await claimIssue(issueAdapter, config, options.issueNumber, 'plan-parent', now);
|
|
47
48
|
try {
|
|
48
49
|
await git.createIssueWorktree({
|
|
@@ -74,7 +75,7 @@ export async function runPlanAutoCommand(options) {
|
|
|
74
75
|
sessionId,
|
|
75
76
|
promptText,
|
|
76
77
|
});
|
|
77
|
-
|
|
78
|
+
store = new RunnerStateStore(targetRoot, config);
|
|
78
79
|
await store.upsertRun({
|
|
79
80
|
issueNumber: options.issueNumber,
|
|
80
81
|
mode: 'plan-parent',
|
|
@@ -130,15 +131,14 @@ export async function runPlanAutoCommand(options) {
|
|
|
130
131
|
body: report.parent.body,
|
|
131
132
|
});
|
|
132
133
|
for (const node of topologicalPlanNodes(report.graph)) {
|
|
133
|
-
const persisted = await
|
|
134
|
+
const persisted = await persistAutonomousChildNode(issueAdapter, config, options.issueNumber, node);
|
|
134
135
|
childIssues.push(persisted);
|
|
135
136
|
}
|
|
136
|
-
const childNodes = await
|
|
137
|
+
const childNodes = await readAutonomousChildNodes(issueAdapter, config, options.issueNumber, childIssues);
|
|
137
138
|
const batches = collectExecutableChildBatches(childNodes, config);
|
|
138
139
|
if (!batches.ok) {
|
|
139
140
|
return finishPlanBlocked(issueAdapter, config, base, batches.errors, childIssues);
|
|
140
141
|
}
|
|
141
|
-
const executionResults = [];
|
|
142
142
|
for (const batch of batches.batches) {
|
|
143
143
|
const settled = await Promise.allSettled(batch.map((child) => executeChild({
|
|
144
144
|
targetRoot,
|
|
@@ -167,6 +167,7 @@ export async function runPlanAutoCommand(options) {
|
|
|
167
167
|
if (failures.length > 0) {
|
|
168
168
|
const successful = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
169
169
|
await blockFailedBatch(issueAdapter, config, options.issueNumber, failures, successful);
|
|
170
|
+
await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, 'A later child batch failed before parent publication.', worktreePath);
|
|
170
171
|
return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [
|
|
171
172
|
'Child batch failed before merge; no child from the failed batch was merged, pushed, or published.',
|
|
172
173
|
...failures.map((failure) => failure.child ? `#${failure.child.issue.number}: ${failure.message}` : failure.message),
|
|
@@ -185,6 +186,7 @@ export async function runPlanAutoCommand(options) {
|
|
|
185
186
|
catch (error) {
|
|
186
187
|
if (error instanceof GitMergeConflictError) {
|
|
187
188
|
await handleMergeConflict(git, issueAdapter, config, options.issueNumber, error, childResult, orderedBatchResults);
|
|
189
|
+
await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, `A later merge conflict stopped parent publication before ${branchName} was pushed.`, worktreePath);
|
|
188
190
|
return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [
|
|
189
191
|
`Merge conflict while merging #${childResult.child.issue.number} from ${childResult.branchName}.`,
|
|
190
192
|
error.stderr || error.stdout,
|
|
@@ -195,22 +197,38 @@ export async function runPlanAutoCommand(options) {
|
|
|
195
197
|
}
|
|
196
198
|
for (const childResult of orderedBatchResults) {
|
|
197
199
|
await git.removeWorktree({ targetRoot, worktreePath: childResult.worktreePath });
|
|
198
|
-
await issueAdapter.removeLabels(childResult.child.issue.number, [config.github.labels.running.name]);
|
|
199
|
-
await issueAdapter.addLabels(childResult.child.issue.number, [config.github.labels.review.name]);
|
|
200
|
-
await issueAdapter.postComment(childResult.child.issue.number, buildChildReviewReport(options.issueNumber, childResult));
|
|
201
|
-
await store.removeRun(childResult.child.issue.number);
|
|
202
200
|
executionResults.push(childResult);
|
|
203
201
|
}
|
|
204
202
|
}
|
|
205
203
|
const finalValidation = await runConfiguredChecks(config, worktreePath, shellExecutor, []);
|
|
204
|
+
const failedFinalValidation = finalValidation.filter((line) => line.status === 'failed');
|
|
205
|
+
if (failedFinalValidation.length > 0) {
|
|
206
|
+
await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, 'Parent integration configured checks failed before publication.', worktreePath);
|
|
207
|
+
return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [
|
|
208
|
+
'One or more configured checks failed.',
|
|
209
|
+
...failedFinalValidation.map((line) => `${line.command}: ${line.status} - ${line.summary}`),
|
|
210
|
+
], childIssues);
|
|
211
|
+
}
|
|
206
212
|
await git.pushBranch({ worktreePath, branchName });
|
|
207
213
|
pullRequest = await pullRequestAdapter.createDraftPullRequest({
|
|
208
214
|
title: renderParentTemplate(config.pullRequests.issueTreeTitle, options.issueNumber),
|
|
209
|
-
body: buildIssueTreePullRequestBody(
|
|
215
|
+
body: buildIssueTreePullRequestBody({
|
|
216
|
+
parentIssueNumber: options.issueNumber,
|
|
217
|
+
childIssues,
|
|
218
|
+
childResults: executionResults,
|
|
219
|
+
finalValidation,
|
|
220
|
+
}),
|
|
210
221
|
headBranch: branchName,
|
|
211
222
|
baseBranch: config.branches.base,
|
|
212
223
|
});
|
|
213
|
-
const reportComment = buildIssueTreeReviewReport(
|
|
224
|
+
const reportComment = buildIssueTreeReviewReport({
|
|
225
|
+
parentIssueNumber: options.issueNumber,
|
|
226
|
+
pullRequest,
|
|
227
|
+
batches: batches.batches,
|
|
228
|
+
childResults: executionResults,
|
|
229
|
+
finalValidation,
|
|
230
|
+
});
|
|
231
|
+
await markCompletedChildrenReviewReady(issueAdapter, config, store, options.issueNumber, executionResults);
|
|
214
232
|
await issueAdapter.removeLabels(options.issueNumber, [config.github.labels.running.name]);
|
|
215
233
|
await issueAdapter.addLabels(options.issueNumber, [config.github.labels.review.name]);
|
|
216
234
|
await issueAdapter.postComment(options.issueNumber, reportComment);
|
|
@@ -227,46 +245,11 @@ export async function runPlanAutoCommand(options) {
|
|
|
227
245
|
if (pullRequest) {
|
|
228
246
|
throw new Error(`Issue-tree execution failed after draft PR creation (${pullRequest.url}); not marking parent blocked: ${message}`);
|
|
229
247
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
async function persistChildNode(issueAdapter, config, parentIssueNumber, node) {
|
|
234
|
-
const body = buildChildBody(node, parentIssueNumber);
|
|
235
|
-
const childLabel = config.github.labels.child.name;
|
|
236
|
-
const autoLabel = config.github.labels.auto.name;
|
|
237
|
-
let issue;
|
|
238
|
-
if (node.issueNumber !== undefined) {
|
|
239
|
-
const existing = await issueAdapter.getIssue(node.issueNumber);
|
|
240
|
-
if (!existing) {
|
|
241
|
-
throw new Error(`Existing issue #${node.issueNumber} was not found`);
|
|
242
|
-
}
|
|
243
|
-
if (!isAutonomousChildOfParent(existing, config, parentIssueNumber)) {
|
|
244
|
-
throw new Error(`Existing issue #${node.issueNumber} is not an autonomous child of #${parentIssueNumber}; refusing to update arbitrary issue.`);
|
|
245
|
-
}
|
|
246
|
-
issue = await issueAdapter.updateIssue(node.issueNumber, {
|
|
247
|
-
title: node.title,
|
|
248
|
-
body,
|
|
249
|
-
addLabels: [childLabel],
|
|
250
|
-
removeLabels: node.afkHitl === 'hitl' ? [autoLabel] : undefined,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
issue = await issueAdapter.createIssue({
|
|
255
|
-
title: node.title,
|
|
256
|
-
body,
|
|
257
|
-
labels: [childLabel],
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
if (!isAutonomousChildOfParent(issue, config, parentIssueNumber)) {
|
|
261
|
-
throw new Error(`Child issue #${issue.number} was not persisted with the autonomous marker for #${parentIssueNumber}.`);
|
|
262
|
-
}
|
|
263
|
-
if (node.afkHitl === 'afk') {
|
|
264
|
-
issue = await issueAdapter.updateIssue(issue.number, { addLabels: [autoLabel] });
|
|
265
|
-
if (!isAutonomousChildOfParent(issue, config, parentIssueNumber)) {
|
|
266
|
-
throw new Error(`Child issue #${issue.number} lost the autonomous marker for #${parentIssueNumber} while enabling agent:auto.`);
|
|
248
|
+
if (store && executionResults.length > 0) {
|
|
249
|
+
await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, `Parent publication failed before review handoff: ${message}`, worktreePath);
|
|
267
250
|
}
|
|
251
|
+
return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [message], childIssues);
|
|
268
252
|
}
|
|
269
|
-
return issue;
|
|
270
253
|
}
|
|
271
254
|
async function readPlanReport(reportPath) {
|
|
272
255
|
try {
|
|
@@ -303,27 +286,6 @@ async function readWorkflowPrompt(targetRoot, promptPath, promptName) {
|
|
|
303
286
|
throw error;
|
|
304
287
|
}
|
|
305
288
|
}
|
|
306
|
-
async function readExecutableChildren(issueAdapter, config, parentIssueNumber, childIssues) {
|
|
307
|
-
const nodes = [];
|
|
308
|
-
const errors = [];
|
|
309
|
-
for (const child of childIssues) {
|
|
310
|
-
const current = await issueAdapter.getIssue(child.number);
|
|
311
|
-
if (!current) {
|
|
312
|
-
errors.push(`Child issue #${child.number} was not found during execution readback`);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
const parsed = parseAutonomousChildMetadata(current, config, parentIssueNumber);
|
|
316
|
-
if (!parsed.ok) {
|
|
317
|
-
errors.push(...parsed.errors);
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
nodes.push(parsed.node);
|
|
321
|
-
}
|
|
322
|
-
if (errors.length > 0) {
|
|
323
|
-
throw new Error(errors.join('; '));
|
|
324
|
-
}
|
|
325
|
-
return nodes;
|
|
326
|
-
}
|
|
327
289
|
async function executeChild(input) {
|
|
328
290
|
const childIssueNumber = input.child.issue.number;
|
|
329
291
|
const branchName = `codex/tree-${input.parentIssue.number}-issue-${childIssueNumber}`;
|
|
@@ -413,74 +375,28 @@ async function executeChild(input) {
|
|
|
413
375
|
await cleanupSessionCodexHome(isolatedHomePath);
|
|
414
376
|
}
|
|
415
377
|
const afterHead = await input.git.getHead(worktreePath);
|
|
416
|
-
const
|
|
417
|
-
? []
|
|
418
|
-
: validateNoAgentOwnedGitPublication(beforeHead, afterHead);
|
|
419
|
-
if (publicationViolations.length > 0) {
|
|
420
|
-
throw new Error(publicationViolations.map((violation) => violation.message).join('; '));
|
|
421
|
-
}
|
|
422
|
-
if (codexResult.exitCode !== 0) {
|
|
423
|
-
throw new Error(`Codex exited with code ${codexResult.exitCode}: ${codexResult.stderr || codexResult.stdout}`);
|
|
424
|
-
}
|
|
425
|
-
const report = await readRequiredScopedReport(reportPath);
|
|
426
|
-
if (report.status === 'needs-promotion') {
|
|
427
|
-
const promotion = report.promotion;
|
|
428
|
-
throw new Error([
|
|
429
|
-
'Child requested promotion instead of completing issue-tree work.',
|
|
430
|
-
promotion ? `Reason: ${promotion.reason}` : undefined,
|
|
431
|
-
promotion ? `Evidence: ${promotion.evidence.join(', ')}` : undefined,
|
|
432
|
-
].filter(Boolean).join(' '));
|
|
433
|
-
}
|
|
434
|
-
let changeSet = await input.git.collectSessionChangeSet({ worktreePath, baseHead: beforeHead });
|
|
435
|
-
let changedFiles = changeSet.changedPaths;
|
|
436
|
-
if (changedFiles.length === 0) {
|
|
437
|
-
throw new Error('Codex completed without file changes');
|
|
438
|
-
}
|
|
439
|
-
const violations = [
|
|
440
|
-
...validateChangedPaths(changedFiles, input.config),
|
|
441
|
-
...validateCompletionReportSafety(report),
|
|
442
|
-
];
|
|
443
|
-
if (violations.length > 0) {
|
|
444
|
-
throw new Error(violations.map((violation) => violation.message).join('; '));
|
|
445
|
-
}
|
|
446
|
-
let validation = await runConfiguredChecks(input.config, worktreePath, input.shellExecutor, report.validation);
|
|
447
|
-
const runnerVisualProof = await runRunnerVisualProof({
|
|
378
|
+
const publishability = await runImplementationPublishabilityCheck({
|
|
448
379
|
config: input.config,
|
|
449
380
|
issue: input.child.issue,
|
|
450
|
-
issueNumber: childIssueNumber,
|
|
451
381
|
worktreePath,
|
|
452
|
-
|
|
453
|
-
|
|
382
|
+
reportPath,
|
|
383
|
+
beforeHead,
|
|
384
|
+
afterHead,
|
|
385
|
+
codexResult,
|
|
386
|
+
git: input.git,
|
|
454
387
|
shellExecutor: input.shellExecutor,
|
|
388
|
+
commitMessage: `Codex: implement issue #${childIssueNumber} for parent #${input.parentIssue.number}`,
|
|
455
389
|
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (validation.some((line) => line.status === 'failed')) {
|
|
464
|
-
residualRisks.push('One or more configured checks failed.');
|
|
465
|
-
}
|
|
466
|
-
const reviewGate = evaluateReviewGates({
|
|
467
|
-
config: input.config,
|
|
468
|
-
issue: input.child.issue,
|
|
469
|
-
changedFiles,
|
|
470
|
-
validation,
|
|
471
|
-
skippedChecks: report.skippedChecks,
|
|
472
|
-
report: { ...report, artifacts },
|
|
473
|
-
worktreePath,
|
|
474
|
-
});
|
|
475
|
-
if (!reviewGate.ok) {
|
|
476
|
-
throw new Error(reviewGate.reasons.join('; '));
|
|
390
|
+
if (publishability.status === 'promotion-requested') {
|
|
391
|
+
const promotion = publishability.report.promotion;
|
|
392
|
+
throw new Error([
|
|
393
|
+
'Child requested promotion instead of completing issue-tree work.',
|
|
394
|
+
promotion ? `Reason: ${promotion.reason}` : undefined,
|
|
395
|
+
promotion ? `Evidence: ${promotion.evidence.join(', ')}` : undefined,
|
|
396
|
+
].filter(Boolean).join(' '));
|
|
477
397
|
}
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
worktreePath,
|
|
481
|
-
message: `Codex: implement issue #${childIssueNumber} for parent #${input.parentIssue.number}`,
|
|
482
|
-
});
|
|
483
|
-
changeSet = await input.git.collectSessionChangeSet({ worktreePath, baseHead: beforeHead });
|
|
398
|
+
if (publishability.status === 'blocked') {
|
|
399
|
+
throw new Error(publishability.reasons.join('; '));
|
|
484
400
|
}
|
|
485
401
|
return {
|
|
486
402
|
child: input.child,
|
|
@@ -489,33 +405,14 @@ async function executeChild(input) {
|
|
|
489
405
|
promptPath,
|
|
490
406
|
reportPath,
|
|
491
407
|
logPath,
|
|
492
|
-
changedFiles,
|
|
493
|
-
validation,
|
|
494
|
-
artifacts,
|
|
495
|
-
commits:
|
|
496
|
-
skippedChecks:
|
|
497
|
-
residualRisks,
|
|
408
|
+
changedFiles: publishability.changedFiles,
|
|
409
|
+
validation: publishability.validation,
|
|
410
|
+
artifacts: publishability.artifacts,
|
|
411
|
+
commits: publishability.commits,
|
|
412
|
+
skippedChecks: publishability.skippedChecks,
|
|
413
|
+
residualRisks: publishability.residualRisks,
|
|
498
414
|
};
|
|
499
415
|
}
|
|
500
|
-
async function readRequiredScopedReport(reportPath) {
|
|
501
|
-
const reportRead = await readScopedCompletionReport(reportPath);
|
|
502
|
-
if (reportRead.kind === 'missing') {
|
|
503
|
-
throw new Error('Codex did not write CODEX_ORCHESTRATOR_REPORT_FILE; runner cannot prove child safety contract.');
|
|
504
|
-
}
|
|
505
|
-
return reportRead.report;
|
|
506
|
-
}
|
|
507
|
-
async function runConfiguredChecks(config, worktreePath, shellExecutor, reportValidation) {
|
|
508
|
-
const lines = [...reportValidation];
|
|
509
|
-
for (const [name, command] of Object.entries(config.checks)) {
|
|
510
|
-
const result = await shellExecutor(command, { cwd: worktreePath });
|
|
511
|
-
lines.push({
|
|
512
|
-
command,
|
|
513
|
-
status: result.exitCode === 0 ? 'passed' : 'failed',
|
|
514
|
-
summary: `${name}: ${result.exitCode === 0 ? 'passed' : result.stderr || result.stdout || `exit ${result.exitCode}`}`,
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
return lines;
|
|
518
|
-
}
|
|
519
416
|
async function blockFailedBatch(issueAdapter, config, parentIssueNumber, failures, successfulUnmerged) {
|
|
520
417
|
for (const failure of failures) {
|
|
521
418
|
if (!failure.child) {
|
|
@@ -523,23 +420,23 @@ async function blockFailedBatch(issueAdapter, config, parentIssueNumber, failure
|
|
|
523
420
|
}
|
|
524
421
|
await issueAdapter.removeLabels(failure.child.issue.number, [config.github.labels.running.name]);
|
|
525
422
|
await issueAdapter.addLabels(failure.child.issue.number, [config.github.labels.blocked.name]);
|
|
526
|
-
await issueAdapter.postComment(failure.child.issue.number,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
'Worktree preserved for maintainer inspection.',
|
|
531
|
-
|
|
423
|
+
await issueAdapter.postComment(failure.child.issue.number, buildChildBlockedReport({
|
|
424
|
+
parentIssueNumber,
|
|
425
|
+
childIssueNumber: failure.child.issue.number,
|
|
426
|
+
reasons: [failure.message],
|
|
427
|
+
details: ['Worktree preserved for maintainer inspection.'],
|
|
428
|
+
}));
|
|
532
429
|
}
|
|
533
430
|
for (const result of successfulUnmerged) {
|
|
534
431
|
await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
|
|
535
432
|
await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
|
|
536
|
-
await issueAdapter.postComment(result.child.issue.number,
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
'
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
433
|
+
await issueAdapter.postComment(result.child.issue.number, buildChildBlockedReport({
|
|
434
|
+
parentIssueNumber,
|
|
435
|
+
childIssueNumber: result.child.issue.number,
|
|
436
|
+
reasons: ['A sibling child failed before the batch merge; this child branch was not merged.'],
|
|
437
|
+
branchName: result.branchName,
|
|
438
|
+
worktreePath: result.worktreePath,
|
|
439
|
+
}));
|
|
543
440
|
}
|
|
544
441
|
}
|
|
545
442
|
async function handleMergeConflict(git, issueAdapter, config, parentIssueNumber, error, childResult, batchResults) {
|
|
@@ -553,53 +450,64 @@ async function handleMergeConflict(git, issueAdapter, config, parentIssueNumber,
|
|
|
553
450
|
for (const result of batchResults) {
|
|
554
451
|
await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
|
|
555
452
|
await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
|
|
556
|
-
await issueAdapter.postComment(result.child.issue.number,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
453
|
+
await issueAdapter.postComment(result.child.issue.number, buildChildBlockedReport({
|
|
454
|
+
parentIssueNumber,
|
|
455
|
+
childIssueNumber: result.child.issue.number,
|
|
456
|
+
reasons: [
|
|
457
|
+
result.child.issue.number === childResult.child.issue.number
|
|
458
|
+
? `Merge conflict from branch ${childResult.branchName}`
|
|
459
|
+
: `A sibling merge conflict stopped the batch before publication: ${childResult.branchName}`,
|
|
460
|
+
],
|
|
461
|
+
details: [
|
|
462
|
+
`- Parent worktree: ${error.worktreePath}`,
|
|
463
|
+
`- Child worktree: ${result.worktreePath}`,
|
|
464
|
+
`- Child branch preserved: ${result.branchName}`,
|
|
465
|
+
abortMessage ? `- Merge abort also failed: ${abortMessage}` : '- Merge abort completed.',
|
|
466
|
+
],
|
|
467
|
+
batchChildren: batchResults.map((batchResult) => ({
|
|
468
|
+
issueNumber: batchResult.child.issue.number,
|
|
469
|
+
branchName: batchResult.branchName,
|
|
470
|
+
})),
|
|
471
|
+
gitOutput: error.stderr || error.stdout || 'no git output',
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function markCompletedChildrenReviewReady(issueAdapter, config, store, parentIssueNumber, childResults) {
|
|
476
|
+
for (const childResult of childResults) {
|
|
477
|
+
await issueAdapter.removeLabels(childResult.child.issue.number, [config.github.labels.running.name]);
|
|
478
|
+
await issueAdapter.addLabels(childResult.child.issue.number, [config.github.labels.review.name]);
|
|
479
|
+
await issueAdapter.postComment(childResult.child.issue.number, buildChildReviewReport({ parentIssueNumber, result: childResult }));
|
|
480
|
+
await store.removeRun(childResult.child.issue.number);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function blockCompletedChildren(issueAdapter, config, store, parentIssueNumber, childResults, reason, parentWorktreePath) {
|
|
484
|
+
for (const result of childResults) {
|
|
485
|
+
await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
|
|
486
|
+
await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
|
|
487
|
+
await issueAdapter.postComment(result.child.issue.number, buildChildBlockedReport({
|
|
488
|
+
parentIssueNumber,
|
|
489
|
+
childIssueNumber: result.child.issue.number,
|
|
490
|
+
reasons: [reason],
|
|
491
|
+
details: [
|
|
492
|
+
`- Merged child branch was not published: ${result.branchName}`,
|
|
493
|
+
`- Parent worktree preserved: ${parentWorktreePath}`,
|
|
494
|
+
],
|
|
495
|
+
}));
|
|
496
|
+
await store.removeRun(result.child.issue.number);
|
|
571
497
|
}
|
|
572
498
|
}
|
|
573
499
|
async function finishPlanBlocked(issueAdapter, config, result, reasons, mutatedChildren) {
|
|
574
|
-
const reportComment =
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
'Mutated Child Issues',
|
|
581
|
-
...bulletList(mutatedChildren.map((issue) => `#${issue.number} ${issue.title}`)),
|
|
582
|
-
].join('\n');
|
|
500
|
+
const reportComment = buildParentBlockedReport({
|
|
501
|
+
parentIssueNumber: result.parentIssueNumber,
|
|
502
|
+
reasons,
|
|
503
|
+
logPath: result.logPath,
|
|
504
|
+
mutatedChildren,
|
|
505
|
+
});
|
|
583
506
|
await issueAdapter.removeLabels(result.parentIssueNumber, [config.github.labels.running.name]);
|
|
584
507
|
await issueAdapter.addLabels(result.parentIssueNumber, [config.github.labels.blocked.name]);
|
|
585
508
|
await issueAdapter.postComment(result.parentIssueNumber, reportComment);
|
|
586
509
|
return { ...result, status: 'blocked', reportComment };
|
|
587
510
|
}
|
|
588
|
-
function buildChildBody(node, parentIssueNumber) {
|
|
589
|
-
return [
|
|
590
|
-
ensureAutonomousChildBody(node.body, parentIssueNumber),
|
|
591
|
-
'',
|
|
592
|
-
'## codex-orchestrator metadata',
|
|
593
|
-
`Stable ID: ${node.stableId}`,
|
|
594
|
-
`AFK/HITL: ${node.afkHitl}`,
|
|
595
|
-
`Depends on: ${node.dependsOn.length > 0 ? node.dependsOn.join(', ') : 'none'}`,
|
|
596
|
-
'Ownership:',
|
|
597
|
-
...bulletList(node.ownershipScope),
|
|
598
|
-
'Spec gate: wave-level',
|
|
599
|
-
'Verification:',
|
|
600
|
-
...bulletList(node.verification),
|
|
601
|
-
].join('\n');
|
|
602
|
-
}
|
|
603
511
|
function dependencyIssuesFor(child, allChildren) {
|
|
604
512
|
const issuesByStableId = new Map(allChildren.map((node) => [node.metadata.stableId, node.issue]));
|
|
605
513
|
return child.metadata.dependsOn.flatMap((stableId) => {
|
|
@@ -607,102 +515,9 @@ function dependencyIssuesFor(child, allChildren) {
|
|
|
607
515
|
return issue ? [issue] : [];
|
|
608
516
|
});
|
|
609
517
|
}
|
|
610
|
-
function buildChildReviewReport(parentIssueNumber, result) {
|
|
611
|
-
return [
|
|
612
|
-
`codex-orchestrator child review report for #${result.child.issue.number}`,
|
|
613
|
-
`Parent issue: #${parentIssueNumber}`,
|
|
614
|
-
`Integration branch: ${result.branchName}`,
|
|
615
|
-
'Changes',
|
|
616
|
-
...bulletList(result.changedFiles),
|
|
617
|
-
'Validation',
|
|
618
|
-
...result.validation.map((line) => `- ${line.command}: ${line.status} - ${line.summary}`),
|
|
619
|
-
'Proof Artifacts',
|
|
620
|
-
...renderProofArtifacts(result.artifacts),
|
|
621
|
-
'Local Commits',
|
|
622
|
-
...renderCommitEvidence(result.commits),
|
|
623
|
-
'Log',
|
|
624
|
-
...bulletList([result.logPath]),
|
|
625
|
-
'Skipped Checks',
|
|
626
|
-
...bulletList(result.skippedChecks),
|
|
627
|
-
'Residual Risks',
|
|
628
|
-
...bulletList(result.residualRisks),
|
|
629
|
-
].join('\n');
|
|
630
|
-
}
|
|
631
|
-
function buildIssueTreeReviewReport(parentIssueNumber, pullRequest, batches, childResults, finalValidation) {
|
|
632
|
-
return [
|
|
633
|
-
`codex-orchestrator issue-tree review report for #${parentIssueNumber}`,
|
|
634
|
-
'Pull Request',
|
|
635
|
-
`- ${pullRequest.url}`,
|
|
636
|
-
'Execution Batches',
|
|
637
|
-
...batches.map((batch, index) => `- Batch ${index + 1}: ${batch.map((child) => `#${child.issue.number}`).join(', ')}`),
|
|
638
|
-
'Child Issues',
|
|
639
|
-
...childResults.map((result) => `- #${result.child.issue.number} ${result.child.issue.title}: ${result.branchName}`),
|
|
640
|
-
'Validation',
|
|
641
|
-
...childResults.flatMap((result) => result.validation.map((line) => `- #${result.child.issue.number} ${line.command}: ${line.status} - ${line.summary}`)),
|
|
642
|
-
...finalValidation.map((line) => `- final ${line.command}: ${line.status} - ${line.summary}`),
|
|
643
|
-
'Skipped Checks',
|
|
644
|
-
...bulletList(childResults.flatMap((result) => result.skippedChecks)),
|
|
645
|
-
'Proof Artifacts',
|
|
646
|
-
...childResults.flatMap((result) => renderProofArtifacts(result.artifacts).map((line) => `- #${result.child.issue.number} ${line.replace(/^- /, '')}`)),
|
|
647
|
-
'Logs',
|
|
648
|
-
...childResults.map((result) => `- #${result.child.issue.number}: ${result.logPath}`),
|
|
649
|
-
'Local Commits',
|
|
650
|
-
...childResults.flatMap((result) => renderCommitEvidence(result.commits).map((line) => `- #${result.child.issue.number} ${line.replace(/^- /, '')}`)),
|
|
651
|
-
'Residual Risks',
|
|
652
|
-
...bulletList(childResults.flatMap((result) => result.residualRisks)),
|
|
653
|
-
].join('\n');
|
|
654
|
-
}
|
|
655
|
-
function buildIssueTreePullRequestBody(parentIssueNumber, childIssues, childResults, finalValidation) {
|
|
656
|
-
return [
|
|
657
|
-
`Parent issue: #${parentIssueNumber}`,
|
|
658
|
-
'',
|
|
659
|
-
'Child issues:',
|
|
660
|
-
...childIssues.map((issue) => `- #${issue.number} ${issue.title}`),
|
|
661
|
-
'',
|
|
662
|
-
'Changed files:',
|
|
663
|
-
...childResults.flatMap((result) => [
|
|
664
|
-
`- #${result.child.issue.number}:`,
|
|
665
|
-
...result.changedFiles.map((file) => ` - ${file}`),
|
|
666
|
-
]),
|
|
667
|
-
'',
|
|
668
|
-
'Validation:',
|
|
669
|
-
...childResults.flatMap((result) => result.validation.map((line) => `- #${result.child.issue.number} ${line.command}: ${line.status} - ${line.summary}`)),
|
|
670
|
-
...finalValidation.map((line) => `- final ${line.command}: ${line.status} - ${line.summary}`),
|
|
671
|
-
'',
|
|
672
|
-
'Skipped checks:',
|
|
673
|
-
...bulletList(childResults.flatMap((result) => result.skippedChecks)),
|
|
674
|
-
'',
|
|
675
|
-
'Proof artifacts:',
|
|
676
|
-
...childResults.flatMap((result) => renderProofArtifacts(result.artifacts).map((line) => `- #${result.child.issue.number} ${line.replace(/^- /, '')}`)),
|
|
677
|
-
'',
|
|
678
|
-
'Logs:',
|
|
679
|
-
...childResults.map((result) => `- #${result.child.issue.number}: ${result.logPath}`),
|
|
680
|
-
'',
|
|
681
|
-
'Local commits:',
|
|
682
|
-
...childResults.flatMap((result) => renderCommitEvidence(result.commits).map((line) => `- #${result.child.issue.number} ${line.replace(/^- /, '')}`)),
|
|
683
|
-
'',
|
|
684
|
-
'Residual risks:',
|
|
685
|
-
...bulletList(childResults.flatMap((result) => result.residualRisks)),
|
|
686
|
-
'',
|
|
687
|
-
'Merge summary:',
|
|
688
|
-
...childResults.map((result) => `- ${result.branchName} merged for #${result.child.issue.number}`),
|
|
689
|
-
'',
|
|
690
|
-
'Auto-merge is disabled.',
|
|
691
|
-
].join('\n');
|
|
692
|
-
}
|
|
693
518
|
function renderParentTemplate(template, parentIssueNumber) {
|
|
694
519
|
return template.replaceAll('${parentIssueNumber}', String(parentIssueNumber));
|
|
695
520
|
}
|
|
696
|
-
function renderProofArtifacts(artifacts) {
|
|
697
|
-
if (artifacts.length === 0) {
|
|
698
|
-
return ['- none'];
|
|
699
|
-
}
|
|
700
|
-
return artifacts.map((artifact) => {
|
|
701
|
-
const target = artifact.url ?? artifact.path ?? 'missing-target';
|
|
702
|
-
const label = `${artifact.type}: ${artifact.description}`;
|
|
703
|
-
return artifact.type === 'screenshot' ? `- ![${label.replace(/[\[\]]/g, '')}](${target})` : `- ${label}: ${target}`;
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
521
|
function baseResult(parentIssueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues) {
|
|
707
522
|
return { parentIssueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues };
|
|
708
523
|
}
|