codex-orchestrator 0.1.19 → 0.1.21

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 (92) hide show
  1. package/README.md +132 -33
  2. package/dist/src/codex/command-adapter.d.ts +3 -0
  3. package/dist/src/codex/command-adapter.d.ts.map +1 -1
  4. package/dist/src/codex/command-adapter.js +18 -7
  5. package/dist/src/codex/command-adapter.js.map +1 -1
  6. package/dist/src/config/schema.d.ts +3 -0
  7. package/dist/src/config/schema.d.ts.map +1 -1
  8. package/dist/src/config/schema.js +3 -0
  9. package/dist/src/config/schema.js.map +1 -1
  10. package/dist/src/git/worktree.d.ts +17 -0
  11. package/dist/src/git/worktree.d.ts.map +1 -1
  12. package/dist/src/git/worktree.js +35 -2
  13. package/dist/src/git/worktree.js.map +1 -1
  14. package/dist/src/github/gh-issue-adapter.js +6 -3
  15. package/dist/src/github/gh-issue-adapter.js.map +1 -1
  16. package/dist/src/github/issues.d.ts +1 -1
  17. package/dist/src/github/issues.d.ts.map +1 -1
  18. package/dist/src/index.d.ts +6 -2
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +3 -1
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/path-policy.d.ts +27 -0
  23. package/dist/src/path-policy.d.ts.map +1 -0
  24. package/dist/src/path-policy.js +83 -0
  25. package/dist/src/path-policy.js.map +1 -0
  26. package/dist/src/process/command.d.ts +3 -0
  27. package/dist/src/process/command.d.ts.map +1 -1
  28. package/dist/src/process/command.js +59 -9
  29. package/dist/src/process/command.js.map +1 -1
  30. package/dist/src/runner/command-utils.d.ts +5 -0
  31. package/dist/src/runner/command-utils.d.ts.map +1 -1
  32. package/dist/src/runner/command-utils.js +46 -1
  33. package/dist/src/runner/command-utils.js.map +1 -1
  34. package/dist/src/runner/completion-report.d.ts +54 -0
  35. package/dist/src/runner/completion-report.d.ts.map +1 -0
  36. package/dist/src/runner/completion-report.js +220 -0
  37. package/dist/src/runner/completion-report.js.map +1 -0
  38. package/dist/src/runner/handoff-evidence.d.ts +105 -0
  39. package/dist/src/runner/handoff-evidence.d.ts.map +1 -0
  40. package/dist/src/runner/handoff-evidence.js +224 -0
  41. package/dist/src/runner/handoff-evidence.js.map +1 -0
  42. package/dist/src/runner/issue-tree.d.ts +4 -1
  43. package/dist/src/runner/issue-tree.d.ts.map +1 -1
  44. package/dist/src/runner/issue-tree.js +75 -0
  45. package/dist/src/runner/issue-tree.js.map +1 -1
  46. package/dist/src/runner/local-execution-session.d.ts +79 -0
  47. package/dist/src/runner/local-execution-session.d.ts.map +1 -0
  48. package/dist/src/runner/local-execution-session.js +236 -0
  49. package/dist/src/runner/local-execution-session.js.map +1 -0
  50. package/dist/src/runner/local-state.d.ts +1 -0
  51. package/dist/src/runner/local-state.d.ts.map +1 -1
  52. package/dist/src/runner/local-state.js +2 -1
  53. package/dist/src/runner/local-state.js.map +1 -1
  54. package/dist/src/runner/plan-auto-command.d.ts +1 -0
  55. package/dist/src/runner/plan-auto-command.d.ts.map +1 -1
  56. package/dist/src/runner/plan-auto-command.js +143 -302
  57. package/dist/src/runner/plan-auto-command.js.map +1 -1
  58. package/dist/src/runner/prompt.d.ts +2 -53
  59. package/dist/src/runner/prompt.d.ts.map +1 -1
  60. package/dist/src/runner/prompt.js +20 -265
  61. package/dist/src/runner/prompt.js.map +1 -1
  62. package/dist/src/runner/review-gate-policy.d.ts +24 -0
  63. package/dist/src/runner/review-gate-policy.d.ts.map +1 -0
  64. package/dist/src/runner/review-gate-policy.js +104 -0
  65. package/dist/src/runner/review-gate-policy.js.map +1 -0
  66. package/dist/src/runner/review-gates.d.ts +4 -9
  67. package/dist/src/runner/review-gates.d.ts.map +1 -1
  68. package/dist/src/runner/review-gates.js +17 -63
  69. package/dist/src/runner/review-gates.js.map +1 -1
  70. package/dist/src/runner/run-log.d.ts +18 -0
  71. package/dist/src/runner/run-log.d.ts.map +1 -0
  72. package/dist/src/runner/run-log.js +53 -0
  73. package/dist/src/runner/run-log.js.map +1 -0
  74. package/dist/src/runner/safety.d.ts +1 -1
  75. package/dist/src/runner/safety.d.ts.map +1 -1
  76. package/dist/src/runner/safety.js +5 -20
  77. package/dist/src/runner/safety.js.map +1 -1
  78. package/dist/src/runner/scoped-auto-command.d.ts +4 -0
  79. package/dist/src/runner/scoped-auto-command.d.ts.map +1 -1
  80. package/dist/src/runner/scoped-auto-command.js +70 -172
  81. package/dist/src/runner/scoped-auto-command.js.map +1 -1
  82. package/dist/src/runner/visual-proof-runner.d.ts +3 -8
  83. package/dist/src/runner/visual-proof-runner.d.ts.map +1 -1
  84. package/dist/src/runner/visual-proof-runner.js +71 -17
  85. package/dist/src/runner/visual-proof-runner.js.map +1 -1
  86. package/dist/src/setup/project-config.d.ts.map +1 -1
  87. package/dist/src/setup/project-config.js +5 -0
  88. package/dist/src/setup/project-config.js.map +1 -1
  89. package/dist/src/setup/setup-command.d.ts.map +1 -1
  90. package/dist/src/setup/setup-command.js +42 -0
  91. package/dist/src/setup/setup-command.js.map +1 -1
  92. package/package.json +2 -1
@@ -5,15 +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, readRunnerConfig } from './command-utils.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';
9
11
  import { claimIssue, discoverIssueWork } from './issue-state-machine.js';
10
- 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';
11
14
  import { RunnerStateStore } from './local-state.js';
12
- import { buildIssueTreeChildPrompt, buildPlanAutoPrompt, readScopedCompletionReport, readPlanAutoCompletionReport, sessionPromptPath, sessionReportPath, writeDurablePrompt, } from './prompt.js';
13
- import { evaluateReviewGates } from './review-gates.js';
14
- import { validateChangedPaths, validateCompletionReportSafety, validateNoAgentOwnedGitPublication, } from './safety.js';
15
+ import { buildIssueTreeChildPrompt, buildPlanAutoPrompt, sessionPromptPath, sessionReportPath, writeDurablePrompt, } from './prompt.js';
16
+ import { sessionLogPath } from './run-log.js';
15
17
  import { cleanupSessionCodexHome, sessionCodexHomePath } from './session-home.js';
16
- import { runRunnerVisualProof } from './visual-proof-runner.js';
17
18
  export async function runPlanAutoCommand(options) {
18
19
  const targetRoot = resolve(options.targetRoot);
19
20
  const now = options.now ?? new Date();
@@ -38,8 +39,11 @@ export async function runPlanAutoCommand(options) {
38
39
  const worktreePath = join(targetRoot, config.runner.workspaceRoot, `tree-${options.issueNumber}`);
39
40
  let promptPath = '';
40
41
  let reportPath = '';
42
+ let logPath = '';
41
43
  const childIssues = [];
42
44
  let pullRequest;
45
+ let store;
46
+ const executionResults = [];
43
47
  await claimIssue(issueAdapter, config, options.issueNumber, 'plan-parent', now);
44
48
  try {
45
49
  await git.createIssueWorktree({
@@ -51,6 +55,7 @@ export async function runPlanAutoCommand(options) {
51
55
  const sessionId = `plan-${options.issueNumber}-${formatSessionTimestamp(now)}`;
52
56
  promptPath = sessionPromptPath({ targetRoot, config, issueNumber: options.issueNumber, sessionId });
53
57
  reportPath = sessionReportPath({ targetRoot, config, issueNumber: options.issueNumber, sessionId });
58
+ logPath = sessionLogPath({ targetRoot, config, issueNumber: options.issueNumber, sessionId });
54
59
  const isolatedHomePath = sessionCodexHomePath({ targetRoot, sessionId });
55
60
  await mkdir(dirname(reportPath), { recursive: true });
56
61
  await mkdir(isolatedHomePath, { recursive: true });
@@ -70,7 +75,7 @@ export async function runPlanAutoCommand(options) {
70
75
  sessionId,
71
76
  promptText,
72
77
  });
73
- const store = new RunnerStateStore(targetRoot, config);
78
+ store = new RunnerStateStore(targetRoot, config);
74
79
  await store.upsertRun({
75
80
  issueNumber: options.issueNumber,
76
81
  mode: 'plan-parent',
@@ -79,6 +84,7 @@ export async function runPlanAutoCommand(options) {
79
84
  branchName,
80
85
  promptPath,
81
86
  reportPath,
87
+ logPath,
82
88
  retryCount: 0,
83
89
  createdAt: now.toISOString(),
84
90
  updatedAt: now.toISOString(),
@@ -97,13 +103,14 @@ export async function runPlanAutoCommand(options) {
97
103
  issueNumber: options.issueNumber,
98
104
  sessionId,
99
105
  branchName,
106
+ logPath,
100
107
  });
101
108
  }
102
109
  finally {
103
110
  await cleanupSessionCodexHome(isolatedHomePath);
104
111
  }
105
112
  const afterHead = await git.getHead(worktreePath);
106
- const base = baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, childIssues);
113
+ const base = baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues);
107
114
  if (beforeHead !== afterHead) {
108
115
  return finishPlanBlocked(issueAdapter, config, base, ['Planning session changed git HEAD; planning must not commit.'], []);
109
116
  }
@@ -124,15 +131,14 @@ export async function runPlanAutoCommand(options) {
124
131
  body: report.parent.body,
125
132
  });
126
133
  for (const node of topologicalPlanNodes(report.graph)) {
127
- const persisted = await persistChildNode(issueAdapter, config, options.issueNumber, node);
134
+ const persisted = await persistAutonomousChildNode(issueAdapter, config, options.issueNumber, node);
128
135
  childIssues.push(persisted);
129
136
  }
130
- const childNodes = await readExecutableChildren(issueAdapter, config, options.issueNumber, childIssues);
137
+ const childNodes = await readAutonomousChildNodes(issueAdapter, config, options.issueNumber, childIssues);
131
138
  const batches = collectExecutableChildBatches(childNodes, config);
132
139
  if (!batches.ok) {
133
140
  return finishPlanBlocked(issueAdapter, config, base, batches.errors, childIssues);
134
141
  }
135
- const executionResults = [];
136
142
  for (const batch of batches.batches) {
137
143
  const settled = await Promise.allSettled(batch.map((child) => executeChild({
138
144
  targetRoot,
@@ -161,7 +167,8 @@ export async function runPlanAutoCommand(options) {
161
167
  if (failures.length > 0) {
162
168
  const successful = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
163
169
  await blockFailedBatch(issueAdapter, config, options.issueNumber, failures, successful);
164
- return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, childIssues), [
170
+ await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, 'A later child batch failed before parent publication.', worktreePath);
171
+ return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [
165
172
  'Child batch failed before merge; no child from the failed batch was merged, pushed, or published.',
166
173
  ...failures.map((failure) => failure.child ? `#${failure.child.issue.number}: ${failure.message}` : failure.message),
167
174
  ], childIssues);
@@ -179,7 +186,8 @@ export async function runPlanAutoCommand(options) {
179
186
  catch (error) {
180
187
  if (error instanceof GitMergeConflictError) {
181
188
  await handleMergeConflict(git, issueAdapter, config, options.issueNumber, error, childResult, orderedBatchResults);
182
- return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, childIssues), [
189
+ await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, `A later merge conflict stopped parent publication before ${branchName} was pushed.`, worktreePath);
190
+ return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [
183
191
  `Merge conflict while merging #${childResult.child.issue.number} from ${childResult.branchName}.`,
184
192
  error.stderr || error.stdout,
185
193
  ], childIssues);
@@ -189,28 +197,44 @@ export async function runPlanAutoCommand(options) {
189
197
  }
190
198
  for (const childResult of orderedBatchResults) {
191
199
  await git.removeWorktree({ targetRoot, worktreePath: childResult.worktreePath });
192
- await issueAdapter.removeLabels(childResult.child.issue.number, [config.github.labels.running.name]);
193
- await issueAdapter.addLabels(childResult.child.issue.number, [config.github.labels.review.name]);
194
- await issueAdapter.postComment(childResult.child.issue.number, buildChildReviewReport(options.issueNumber, childResult));
195
- await store.removeRun(childResult.child.issue.number);
196
200
  executionResults.push(childResult);
197
201
  }
198
202
  }
199
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
+ }
200
212
  await git.pushBranch({ worktreePath, branchName });
201
213
  pullRequest = await pullRequestAdapter.createDraftPullRequest({
202
214
  title: renderParentTemplate(config.pullRequests.issueTreeTitle, options.issueNumber),
203
- body: buildIssueTreePullRequestBody(options.issueNumber, childIssues, executionResults, finalValidation),
215
+ body: buildIssueTreePullRequestBody({
216
+ parentIssueNumber: options.issueNumber,
217
+ childIssues,
218
+ childResults: executionResults,
219
+ finalValidation,
220
+ }),
204
221
  headBranch: branchName,
205
222
  baseBranch: config.branches.base,
206
223
  });
207
- 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);
208
232
  await issueAdapter.removeLabels(options.issueNumber, [config.github.labels.running.name]);
209
233
  await issueAdapter.addLabels(options.issueNumber, [config.github.labels.review.name]);
210
234
  await issueAdapter.postComment(options.issueNumber, reportComment);
211
235
  await store.removeRun(options.issueNumber);
212
236
  return {
213
- ...baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, childIssues),
237
+ ...baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues),
214
238
  status: 'review-ready',
215
239
  pullRequest,
216
240
  reportComment,
@@ -221,46 +245,11 @@ export async function runPlanAutoCommand(options) {
221
245
  if (pullRequest) {
222
246
  throw new Error(`Issue-tree execution failed after draft PR creation (${pullRequest.url}); not marking parent blocked: ${message}`);
223
247
  }
224
- return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, childIssues), [message], childIssues);
225
- }
226
- }
227
- async function persistChildNode(issueAdapter, config, parentIssueNumber, node) {
228
- const body = buildChildBody(node, parentIssueNumber);
229
- const childLabel = config.github.labels.child.name;
230
- const autoLabel = config.github.labels.auto.name;
231
- let issue;
232
- if (node.issueNumber !== undefined) {
233
- const existing = await issueAdapter.getIssue(node.issueNumber);
234
- if (!existing) {
235
- throw new Error(`Existing issue #${node.issueNumber} was not found`);
236
- }
237
- if (!isAutonomousChildOfParent(existing, config, parentIssueNumber)) {
238
- throw new Error(`Existing issue #${node.issueNumber} is not an autonomous child of #${parentIssueNumber}; refusing to update arbitrary issue.`);
239
- }
240
- issue = await issueAdapter.updateIssue(node.issueNumber, {
241
- title: node.title,
242
- body,
243
- addLabels: [childLabel],
244
- removeLabels: node.afkHitl === 'hitl' ? [autoLabel] : undefined,
245
- });
246
- }
247
- else {
248
- issue = await issueAdapter.createIssue({
249
- title: node.title,
250
- body,
251
- labels: [childLabel],
252
- });
253
- }
254
- if (!isAutonomousChildOfParent(issue, config, parentIssueNumber)) {
255
- throw new Error(`Child issue #${issue.number} was not persisted with the autonomous marker for #${parentIssueNumber}.`);
256
- }
257
- if (node.afkHitl === 'afk') {
258
- issue = await issueAdapter.updateIssue(issue.number, { addLabels: [autoLabel] });
259
- if (!isAutonomousChildOfParent(issue, config, parentIssueNumber)) {
260
- 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);
261
250
  }
251
+ return finishPlanBlocked(issueAdapter, config, baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues), [message], childIssues);
262
252
  }
263
- return issue;
264
253
  }
265
254
  async function readPlanReport(reportPath) {
266
255
  try {
@@ -297,27 +286,6 @@ async function readWorkflowPrompt(targetRoot, promptPath, promptName) {
297
286
  throw error;
298
287
  }
299
288
  }
300
- async function readExecutableChildren(issueAdapter, config, parentIssueNumber, childIssues) {
301
- const nodes = [];
302
- const errors = [];
303
- for (const child of childIssues) {
304
- const current = await issueAdapter.getIssue(child.number);
305
- if (!current) {
306
- errors.push(`Child issue #${child.number} was not found during execution readback`);
307
- continue;
308
- }
309
- const parsed = parseAutonomousChildMetadata(current, config, parentIssueNumber);
310
- if (!parsed.ok) {
311
- errors.push(...parsed.errors);
312
- continue;
313
- }
314
- nodes.push(parsed.node);
315
- }
316
- if (errors.length > 0) {
317
- throw new Error(errors.join('; '));
318
- }
319
- return nodes;
320
- }
321
289
  async function executeChild(input) {
322
290
  const childIssueNumber = input.child.issue.number;
323
291
  const branchName = `codex/tree-${input.parentIssue.number}-issue-${childIssueNumber}`;
@@ -335,6 +303,12 @@ async function executeChild(input) {
335
303
  issueNumber: childIssueNumber,
336
304
  sessionId,
337
305
  });
306
+ const logPath = sessionLogPath({
307
+ targetRoot: input.targetRoot,
308
+ config: input.config,
309
+ issueNumber: childIssueNumber,
310
+ sessionId,
311
+ });
338
312
  const isolatedHomePath = sessionCodexHomePath({ targetRoot: input.targetRoot, sessionId });
339
313
  const store = new RunnerStateStore(input.targetRoot, input.config);
340
314
  await input.git.createIssueWorktree({
@@ -375,6 +349,7 @@ async function executeChild(input) {
375
349
  branchName,
376
350
  promptPath,
377
351
  reportPath,
352
+ logPath,
378
353
  retryCount: 0,
379
354
  createdAt: input.now.toISOString(),
380
355
  updatedAt: input.now.toISOString(),
@@ -393,119 +368,51 @@ async function executeChild(input) {
393
368
  issueNumber: childIssueNumber,
394
369
  sessionId,
395
370
  branchName,
371
+ logPath,
396
372
  });
397
373
  }
398
374
  finally {
399
375
  await cleanupSessionCodexHome(isolatedHomePath);
400
376
  }
401
377
  const afterHead = await input.git.getHead(worktreePath);
402
- const publicationViolations = validateNoAgentOwnedGitPublication(beforeHead, afterHead);
403
- if (publicationViolations.length > 0) {
404
- throw new Error(publicationViolations.map((violation) => violation.message).join('; '));
405
- }
406
- if (codexResult.exitCode !== 0) {
407
- throw new Error(`Codex exited with code ${codexResult.exitCode}: ${codexResult.stderr || codexResult.stdout}`);
408
- }
409
- const report = await readRequiredScopedReport(reportPath);
410
- if (report.status === 'needs-promotion') {
411
- const promotion = report.promotion;
412
- throw new Error([
413
- 'Child requested promotion instead of completing issue-tree work.',
414
- promotion ? `Reason: ${promotion.reason}` : undefined,
415
- promotion ? `Evidence: ${promotion.evidence.join(', ')}` : undefined,
416
- ].filter(Boolean).join(' '));
417
- }
418
- let changedFiles = await input.git.listChangedFiles(worktreePath);
419
- if (changedFiles.length === 0) {
420
- throw new Error('Codex completed without file changes');
421
- }
422
- const violations = [
423
- ...validateChangedPaths(changedFiles, input.config),
424
- ...validateCompletionReportSafety(report),
425
- ];
426
- if (violations.length > 0) {
427
- throw new Error(violations.map((violation) => violation.message).join('; '));
428
- }
429
- let validation = await runConfiguredChecks(input.config, worktreePath, input.shellExecutor, report.validation);
430
- const runnerVisualProof = await runRunnerVisualProof({
378
+ const publishability = await runImplementationPublishabilityCheck({
431
379
  config: input.config,
432
380
  issue: input.child.issue,
433
- issueNumber: childIssueNumber,
434
381
  worktreePath,
435
- changedFiles,
436
- report,
382
+ reportPath,
383
+ beforeHead,
384
+ afterHead,
385
+ codexResult,
386
+ git: input.git,
437
387
  shellExecutor: input.shellExecutor,
388
+ commitMessage: `Codex: implement issue #${childIssueNumber} for parent #${input.parentIssue.number}`,
438
389
  });
439
- validation = [...validation, ...runnerVisualProof.validation];
440
- const artifacts = mergeArtifacts(report.artifacts, runnerVisualProof.artifacts);
441
- if (runnerVisualProof.artifacts.length > 0) {
442
- changedFiles = await input.git.listChangedFiles(worktreePath);
443
- }
444
- const residualRisks = [...report.residualRisks];
445
- if (validation.some((line) => line.status === 'failed')) {
446
- residualRisks.push('One or more configured checks failed.');
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(' '));
447
397
  }
448
- const reviewGate = evaluateReviewGates({
449
- config: input.config,
450
- issue: input.child.issue,
451
- changedFiles,
452
- validation,
453
- skippedChecks: report.skippedChecks,
454
- report: { ...report, artifacts },
455
- worktreePath,
456
- });
457
- if (!reviewGate.ok) {
458
- throw new Error(reviewGate.reasons.join('; '));
398
+ if (publishability.status === 'blocked') {
399
+ throw new Error(publishability.reasons.join('; '));
459
400
  }
460
- await input.git.commitAll({
461
- worktreePath,
462
- message: `Codex: implement issue #${childIssueNumber} for parent #${input.parentIssue.number}`,
463
- });
464
401
  return {
465
402
  child: input.child,
466
403
  branchName,
467
404
  worktreePath,
468
405
  promptPath,
469
406
  reportPath,
470
- changedFiles,
471
- validation,
472
- artifacts,
473
- skippedChecks: report.skippedChecks,
474
- residualRisks,
407
+ logPath,
408
+ changedFiles: publishability.changedFiles,
409
+ validation: publishability.validation,
410
+ artifacts: publishability.artifacts,
411
+ commits: publishability.commits,
412
+ skippedChecks: publishability.skippedChecks,
413
+ residualRisks: publishability.residualRisks,
475
414
  };
476
415
  }
477
- async function readRequiredScopedReport(reportPath) {
478
- const reportRead = await readScopedCompletionReport(reportPath);
479
- if (reportRead.kind === 'missing') {
480
- throw new Error('Codex did not write CODEX_ORCHESTRATOR_REPORT_FILE; runner cannot prove child safety contract.');
481
- }
482
- return reportRead.report;
483
- }
484
- async function runConfiguredChecks(config, worktreePath, shellExecutor, reportValidation) {
485
- const lines = [...reportValidation];
486
- for (const [name, command] of Object.entries(config.checks)) {
487
- const result = await shellExecutor(command, { cwd: worktreePath });
488
- lines.push({
489
- command,
490
- status: result.exitCode === 0 ? 'passed' : 'failed',
491
- summary: `${name}: ${result.exitCode === 0 ? 'passed' : result.stderr || result.stdout || `exit ${result.exitCode}`}`,
492
- });
493
- }
494
- return lines;
495
- }
496
- function mergeArtifacts(existing, additions) {
497
- const seen = new Set(existing.map((artifact) => artifact.url ?? artifact.path ?? artifact.description));
498
- const merged = [...existing];
499
- for (const artifact of additions) {
500
- const key = artifact.url ?? artifact.path ?? artifact.description;
501
- if (seen.has(key)) {
502
- continue;
503
- }
504
- seen.add(key);
505
- merged.push(artifact);
506
- }
507
- return merged;
508
- }
509
416
  async function blockFailedBatch(issueAdapter, config, parentIssueNumber, failures, successfulUnmerged) {
510
417
  for (const failure of failures) {
511
418
  if (!failure.child) {
@@ -513,23 +420,23 @@ async function blockFailedBatch(issueAdapter, config, parentIssueNumber, failure
513
420
  }
514
421
  await issueAdapter.removeLabels(failure.child.issue.number, [config.github.labels.running.name]);
515
422
  await issueAdapter.addLabels(failure.child.issue.number, [config.github.labels.blocked.name]);
516
- await issueAdapter.postComment(failure.child.issue.number, [
517
- `codex-orchestrator blocked child #${failure.child.issue.number} for parent #${parentIssueNumber}`,
518
- 'Reasons',
519
- `- ${failure.message}`,
520
- 'Worktree preserved for maintainer inspection.',
521
- ].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
+ }));
522
429
  }
523
430
  for (const result of successfulUnmerged) {
524
431
  await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
525
432
  await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
526
- await issueAdapter.postComment(result.child.issue.number, [
527
- `codex-orchestrator blocked child #${result.child.issue.number} for parent #${parentIssueNumber}`,
528
- 'Reasons',
529
- '- A sibling child failed before the batch merge; this child branch was not merged.',
530
- `- Branch preserved: ${result.branchName}`,
531
- `- Worktree preserved: ${result.worktreePath}`,
532
- ].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
+ }));
533
440
  }
534
441
  }
535
442
  async function handleMergeConflict(git, issueAdapter, config, parentIssueNumber, error, childResult, batchResults) {
@@ -543,51 +450,64 @@ async function handleMergeConflict(git, issueAdapter, config, parentIssueNumber,
543
450
  for (const result of batchResults) {
544
451
  await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
545
452
  await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
546
- await issueAdapter.postComment(result.child.issue.number, [
547
- `codex-orchestrator blocked child #${result.child.issue.number} for parent #${parentIssueNumber}`,
548
- 'Reasons',
549
- result.child.issue.number === childResult.child.issue.number
550
- ? `- Merge conflict from branch ${childResult.branchName}`
551
- : `- A sibling merge conflict stopped the batch before publication: ${childResult.branchName}`,
552
- `- Parent worktree: ${error.worktreePath}`,
553
- `- Child worktree: ${result.worktreePath}`,
554
- `- Child branch preserved: ${result.branchName}`,
555
- abortMessage ? `- Merge abort also failed: ${abortMessage}` : '- Merge abort completed.',
556
- 'Batch Children',
557
- ...bulletList(batchResults.map((batchResult) => `#${batchResult.child.issue.number} ${batchResult.branchName}`)),
558
- 'Git Output',
559
- ...bulletList([error.stderr || error.stdout || 'no git output']),
560
- ].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);
561
497
  }
562
498
  }
563
499
  async function finishPlanBlocked(issueAdapter, config, result, reasons, mutatedChildren) {
564
- const reportComment = [
565
- `codex-orchestrator blocked parent issue-tree execution for #${result.parentIssueNumber}`,
566
- 'Reasons',
567
- ...bulletList(reasons),
568
- 'Mutated Child Issues',
569
- ...bulletList(mutatedChildren.map((issue) => `#${issue.number} ${issue.title}`)),
570
- ].join('\n');
500
+ const reportComment = buildParentBlockedReport({
501
+ parentIssueNumber: result.parentIssueNumber,
502
+ reasons,
503
+ logPath: result.logPath,
504
+ mutatedChildren,
505
+ });
571
506
  await issueAdapter.removeLabels(result.parentIssueNumber, [config.github.labels.running.name]);
572
507
  await issueAdapter.addLabels(result.parentIssueNumber, [config.github.labels.blocked.name]);
573
508
  await issueAdapter.postComment(result.parentIssueNumber, reportComment);
574
509
  return { ...result, status: 'blocked', reportComment };
575
510
  }
576
- function buildChildBody(node, parentIssueNumber) {
577
- return [
578
- ensureAutonomousChildBody(node.body, parentIssueNumber),
579
- '',
580
- '## codex-orchestrator metadata',
581
- `Stable ID: ${node.stableId}`,
582
- `AFK/HITL: ${node.afkHitl}`,
583
- `Depends on: ${node.dependsOn.length > 0 ? node.dependsOn.join(', ') : 'none'}`,
584
- 'Ownership:',
585
- ...bulletList(node.ownershipScope),
586
- 'Spec gate: wave-level',
587
- 'Verification:',
588
- ...bulletList(node.verification),
589
- ].join('\n');
590
- }
591
511
  function dependencyIssuesFor(child, allChildren) {
592
512
  const issuesByStableId = new Map(allChildren.map((node) => [node.metadata.stableId, node.issue]));
593
513
  return child.metadata.dependsOn.flatMap((stableId) => {
@@ -595,89 +515,10 @@ function dependencyIssuesFor(child, allChildren) {
595
515
  return issue ? [issue] : [];
596
516
  });
597
517
  }
598
- function buildChildReviewReport(parentIssueNumber, result) {
599
- return [
600
- `codex-orchestrator child review report for #${result.child.issue.number}`,
601
- `Parent issue: #${parentIssueNumber}`,
602
- `Integration branch: ${result.branchName}`,
603
- 'Changes',
604
- ...bulletList(result.changedFiles),
605
- 'Validation',
606
- ...result.validation.map((line) => `- ${line.command}: ${line.status} - ${line.summary}`),
607
- 'Proof Artifacts',
608
- ...renderProofArtifacts(result.artifacts),
609
- 'Skipped Checks',
610
- ...bulletList(result.skippedChecks),
611
- 'Residual Risks',
612
- ...bulletList(result.residualRisks),
613
- ].join('\n');
614
- }
615
- function buildIssueTreeReviewReport(parentIssueNumber, pullRequest, batches, childResults, finalValidation) {
616
- return [
617
- `codex-orchestrator issue-tree review report for #${parentIssueNumber}`,
618
- 'Pull Request',
619
- `- ${pullRequest.url}`,
620
- 'Execution Batches',
621
- ...batches.map((batch, index) => `- Batch ${index + 1}: ${batch.map((child) => `#${child.issue.number}`).join(', ')}`),
622
- 'Child Issues',
623
- ...childResults.map((result) => `- #${result.child.issue.number} ${result.child.issue.title}: ${result.branchName}`),
624
- 'Validation',
625
- ...childResults.flatMap((result) => result.validation.map((line) => `- #${result.child.issue.number} ${line.command}: ${line.status} - ${line.summary}`)),
626
- ...finalValidation.map((line) => `- final ${line.command}: ${line.status} - ${line.summary}`),
627
- 'Skipped Checks',
628
- ...bulletList(childResults.flatMap((result) => result.skippedChecks)),
629
- 'Proof Artifacts',
630
- ...childResults.flatMap((result) => renderProofArtifacts(result.artifacts).map((line) => `- #${result.child.issue.number} ${line.replace(/^- /, '')}`)),
631
- 'Residual Risks',
632
- ...bulletList(childResults.flatMap((result) => result.residualRisks)),
633
- ].join('\n');
634
- }
635
- function buildIssueTreePullRequestBody(parentIssueNumber, childIssues, childResults, finalValidation) {
636
- return [
637
- `Parent issue: #${parentIssueNumber}`,
638
- '',
639
- 'Child issues:',
640
- ...childIssues.map((issue) => `- #${issue.number} ${issue.title}`),
641
- '',
642
- 'Changed files:',
643
- ...childResults.flatMap((result) => [
644
- `- #${result.child.issue.number}:`,
645
- ...result.changedFiles.map((file) => ` - ${file}`),
646
- ]),
647
- '',
648
- 'Validation:',
649
- ...childResults.flatMap((result) => result.validation.map((line) => `- #${result.child.issue.number} ${line.command}: ${line.status} - ${line.summary}`)),
650
- ...finalValidation.map((line) => `- final ${line.command}: ${line.status} - ${line.summary}`),
651
- '',
652
- 'Skipped checks:',
653
- ...bulletList(childResults.flatMap((result) => result.skippedChecks)),
654
- '',
655
- 'Proof artifacts:',
656
- ...childResults.flatMap((result) => renderProofArtifacts(result.artifacts).map((line) => `- #${result.child.issue.number} ${line.replace(/^- /, '')}`)),
657
- '',
658
- 'Residual risks:',
659
- ...bulletList(childResults.flatMap((result) => result.residualRisks)),
660
- '',
661
- 'Merge summary:',
662
- ...childResults.map((result) => `- ${result.branchName} merged for #${result.child.issue.number}`),
663
- '',
664
- 'Auto-merge is disabled.',
665
- ].join('\n');
666
- }
667
518
  function renderParentTemplate(template, parentIssueNumber) {
668
519
  return template.replaceAll('${parentIssueNumber}', String(parentIssueNumber));
669
520
  }
670
- function renderProofArtifacts(artifacts) {
671
- if (artifacts.length === 0) {
672
- return ['- none'];
673
- }
674
- return artifacts.map((artifact) => {
675
- const target = artifact.url ?? artifact.path ?? 'missing-target';
676
- const label = `${artifact.type}: ${artifact.description}`;
677
- return artifact.type === 'screenshot' ? `- ![${label.replace(/[\[\]]/g, '')}](${target})` : `- ${label}: ${target}`;
678
- });
679
- }
680
- function baseResult(parentIssueNumber, branchName, worktreePath, promptPath, reportPath, childIssues) {
681
- return { parentIssueNumber, branchName, worktreePath, promptPath, reportPath, childIssues };
521
+ function baseResult(parentIssueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues) {
522
+ return { parentIssueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues };
682
523
  }
683
524
  //# sourceMappingURL=plan-auto-command.js.map