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.
Files changed (61) hide show
  1. package/README.md +18 -13
  2. package/dist/src/config/schema.d.ts +7 -0
  3. package/dist/src/config/schema.d.ts.map +1 -1
  4. package/dist/src/config/schema.js +34 -0
  5. package/dist/src/config/schema.js.map +1 -1
  6. package/dist/src/git/worktree.d.ts.map +1 -1
  7. package/dist/src/git/worktree.js +1 -6
  8. package/dist/src/git/worktree.js.map +1 -1
  9. package/dist/src/path-policy.d.ts +27 -0
  10. package/dist/src/path-policy.d.ts.map +1 -0
  11. package/dist/src/path-policy.js +83 -0
  12. package/dist/src/path-policy.js.map +1 -0
  13. package/dist/src/runner/command-utils.d.ts +3 -2
  14. package/dist/src/runner/command-utils.d.ts.map +1 -1
  15. package/dist/src/runner/command-utils.js +71 -4
  16. package/dist/src/runner/command-utils.js.map +1 -1
  17. package/dist/src/runner/handoff-evidence.d.ts +105 -0
  18. package/dist/src/runner/handoff-evidence.d.ts.map +1 -0
  19. package/dist/src/runner/handoff-evidence.js +224 -0
  20. package/dist/src/runner/handoff-evidence.js.map +1 -0
  21. package/dist/src/runner/issue-tree.d.ts +4 -1
  22. package/dist/src/runner/issue-tree.d.ts.map +1 -1
  23. package/dist/src/runner/issue-tree.js +75 -0
  24. package/dist/src/runner/issue-tree.js.map +1 -1
  25. package/dist/src/runner/local-execution-session.d.ts +45 -0
  26. package/dist/src/runner/local-execution-session.d.ts.map +1 -1
  27. package/dist/src/runner/local-execution-session.js +229 -0
  28. package/dist/src/runner/local-execution-session.js.map +1 -1
  29. package/dist/src/runner/plan-auto-command.d.ts.map +1 -1
  30. package/dist/src/runner/plan-auto-command.js +122 -307
  31. package/dist/src/runner/plan-auto-command.js.map +1 -1
  32. package/dist/src/runner/prompt.d.ts +4 -0
  33. package/dist/src/runner/prompt.d.ts.map +1 -1
  34. package/dist/src/runner/prompt.js +15 -45
  35. package/dist/src/runner/prompt.js.map +1 -1
  36. package/dist/src/runner/review-gate-policy.d.ts +24 -0
  37. package/dist/src/runner/review-gate-policy.d.ts.map +1 -0
  38. package/dist/src/runner/review-gate-policy.js +105 -0
  39. package/dist/src/runner/review-gate-policy.js.map +1 -0
  40. package/dist/src/runner/review-gates.d.ts +4 -8
  41. package/dist/src/runner/review-gates.d.ts.map +1 -1
  42. package/dist/src/runner/review-gates.js +27 -101
  43. package/dist/src/runner/review-gates.js.map +1 -1
  44. package/dist/src/runner/safety.d.ts.map +1 -1
  45. package/dist/src/runner/safety.js +5 -20
  46. package/dist/src/runner/safety.js.map +1 -1
  47. package/dist/src/runner/scoped-auto-command.d.ts.map +1 -1
  48. package/dist/src/runner/scoped-auto-command.js +137 -288
  49. package/dist/src/runner/scoped-auto-command.js.map +1 -1
  50. package/dist/src/runner/visual-proof-runner.d.ts +2 -7
  51. package/dist/src/runner/visual-proof-runner.d.ts.map +1 -1
  52. package/dist/src/runner/visual-proof-runner.js +13 -14
  53. package/dist/src/runner/visual-proof-runner.js.map +1 -1
  54. package/dist/src/setup/project-config.d.ts.map +1 -1
  55. package/dist/src/setup/project-config.js +19 -5
  56. package/dist/src/setup/project-config.js.map +1 -1
  57. package/dist/src/setup/setup-command.d.ts.map +1 -1
  58. package/dist/src/setup/setup-command.js +42 -0
  59. package/dist/src/setup/setup-command.js.map +1 -1
  60. package/package.json +1 -1
  61. 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 { bulletList, formatSessionTimestamp, mergeArtifacts, readRunnerConfig, renderCommitEvidence } from './command-utils.js';
9
- import { readPlanAutoCompletionReport, readScopedCompletionReport, } from './completion-report.js';
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, ensureAutonomousChildBody, isAutonomousChildOfParent, parseAutonomousChildMetadata, topologicalPlanNodes, } from './issue-tree.js';
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
- const store = new RunnerStateStore(targetRoot, config);
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 persistChildNode(issueAdapter, config, options.issueNumber, node);
134
+ const persisted = await persistAutonomousChildNode(issueAdapter, config, options.issueNumber, node);
134
135
  childIssues.push(persisted);
135
136
  }
136
- const childNodes = await readExecutableChildren(issueAdapter, config, options.issueNumber, childIssues);
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(options.issueNumber, childIssues, executionResults, finalValidation),
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(options.issueNumber, pullRequest, batches.batches, executionResults, finalValidation);
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
- return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [message], childIssues);
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 publicationViolations = input.config.runner.allowAgentLocalCommits
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
- changedFiles,
453
- report,
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
- validation = [...validation, ...runnerVisualProof.validation];
457
- const artifacts = mergeArtifacts(report.artifacts, runnerVisualProof.artifacts);
458
- if (runnerVisualProof.artifacts.length > 0) {
459
- changeSet = await input.git.collectSessionChangeSet({ worktreePath, baseHead: beforeHead });
460
- changedFiles = changeSet.changedPaths;
461
- }
462
- const residualRisks = [...report.residualRisks];
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 (!(await input.git.isWorktreeClean(worktreePath))) {
479
- await input.git.commitAll({
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: changeSet.commits,
496
- skippedChecks: report.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
- `codex-orchestrator blocked child #${failure.child.issue.number} for parent #${parentIssueNumber}`,
528
- 'Reasons',
529
- `- ${failure.message}`,
530
- 'Worktree preserved for maintainer inspection.',
531
- ].join('\n'));
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
- `codex-orchestrator blocked child #${result.child.issue.number} for parent #${parentIssueNumber}`,
538
- 'Reasons',
539
- '- A sibling child failed before the batch merge; this child branch was not merged.',
540
- `- Branch preserved: ${result.branchName}`,
541
- `- Worktree preserved: ${result.worktreePath}`,
542
- ].join('\n'));
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
- `codex-orchestrator blocked child #${result.child.issue.number} for parent #${parentIssueNumber}`,
558
- 'Reasons',
559
- result.child.issue.number === childResult.child.issue.number
560
- ? `- Merge conflict from branch ${childResult.branchName}`
561
- : `- A sibling merge conflict stopped the batch before publication: ${childResult.branchName}`,
562
- `- Parent worktree: ${error.worktreePath}`,
563
- `- Child worktree: ${result.worktreePath}`,
564
- `- Child branch preserved: ${result.branchName}`,
565
- abortMessage ? `- Merge abort also failed: ${abortMessage}` : '- Merge abort completed.',
566
- 'Batch Children',
567
- ...bulletList(batchResults.map((batchResult) => `#${batchResult.child.issue.number} ${batchResult.branchName}`)),
568
- 'Git Output',
569
- ...bulletList([error.stderr || error.stdout || 'no git output']),
570
- ].join('\n'));
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
- `codex-orchestrator blocked parent issue-tree execution for #${result.parentIssueNumber}`,
576
- 'Reasons',
577
- ...bulletList(reasons),
578
- 'Log',
579
- ...bulletList([result.logPath]),
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
  }