codex-orchestrator 0.1.39 → 0.1.41
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/CHANGELOG.md +23 -0
- package/README.md +17 -0
- package/dist/src/cli.js +9 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex/command-adapter.d.ts +5 -0
- package/dist/src/codex/command-adapter.d.ts.map +1 -1
- package/dist/src/codex/command-adapter.js +23 -6
- package/dist/src/codex/command-adapter.js.map +1 -1
- package/dist/src/config/schema.d.ts +12 -2
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +70 -3
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/git/worktree.d.ts +3 -1
- package/dist/src/git/worktree.d.ts.map +1 -1
- package/dist/src/git/worktree.js +51 -17
- package/dist/src/git/worktree.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/runner/auto-visual-proof-command.d.ts.map +1 -1
- package/dist/src/runner/auto-visual-proof-command.js +6 -12
- package/dist/src/runner/auto-visual-proof-command.js.map +1 -1
- package/dist/src/runner/command-utils.d.ts +6 -2
- package/dist/src/runner/command-utils.d.ts.map +1 -1
- package/dist/src/runner/command-utils.js +39 -2
- package/dist/src/runner/command-utils.js.map +1 -1
- package/dist/src/runner/completion-report.d.ts +21 -5
- package/dist/src/runner/completion-report.d.ts.map +1 -1
- package/dist/src/runner/completion-report.js +29 -0
- package/dist/src/runner/completion-report.js.map +1 -1
- package/dist/src/runner/daemon-command.d.ts +2 -0
- package/dist/src/runner/daemon-command.d.ts.map +1 -1
- package/dist/src/runner/daemon-command.js +13 -0
- package/dist/src/runner/daemon-command.js.map +1 -1
- package/dist/src/runner/durable-run-summary.d.ts +31 -0
- package/dist/src/runner/durable-run-summary.d.ts.map +1 -1
- package/dist/src/runner/durable-run-summary.js +99 -1
- package/dist/src/runner/durable-run-summary.js.map +1 -1
- package/dist/src/runner/handoff-evidence.d.ts +6 -7
- package/dist/src/runner/handoff-evidence.d.ts.map +1 -1
- package/dist/src/runner/handoff-evidence.js +23 -2
- package/dist/src/runner/handoff-evidence.js.map +1 -1
- package/dist/src/runner/hygiene-cleanup.d.ts +15 -0
- package/dist/src/runner/hygiene-cleanup.d.ts.map +1 -0
- package/dist/src/runner/hygiene-cleanup.js +25 -0
- package/dist/src/runner/hygiene-cleanup.js.map +1 -0
- package/dist/src/runner/issue-tree.d.ts +1 -1
- package/dist/src/runner/issue-tree.d.ts.map +1 -1
- package/dist/src/runner/issue-tree.js +15 -6
- package/dist/src/runner/issue-tree.js.map +1 -1
- package/dist/src/runner/local-execution-session.d.ts +1 -5
- package/dist/src/runner/local-execution-session.d.ts.map +1 -1
- package/dist/src/runner/local-execution-session.js +20 -2
- package/dist/src/runner/local-execution-session.js.map +1 -1
- package/dist/src/runner/plan-auto-command.d.ts +3 -0
- package/dist/src/runner/plan-auto-command.d.ts.map +1 -1
- package/dist/src/runner/plan-auto-command.js +267 -97
- package/dist/src/runner/plan-auto-command.js.map +1 -1
- package/dist/src/runner/plan-auto-recovery.d.ts +83 -0
- package/dist/src/runner/plan-auto-recovery.d.ts.map +1 -0
- package/dist/src/runner/plan-auto-recovery.js +303 -0
- package/dist/src/runner/plan-auto-recovery.js.map +1 -0
- package/dist/src/runner/prompt.d.ts +1 -0
- package/dist/src/runner/prompt.d.ts.map +1 -1
- package/dist/src/runner/prompt.js +5 -2
- package/dist/src/runner/prompt.js.map +1 -1
- package/dist/src/runner/review-gate-policy.d.ts +15 -1
- package/dist/src/runner/review-gate-policy.d.ts.map +1 -1
- package/dist/src/runner/review-gate-policy.js +101 -28
- package/dist/src/runner/review-gate-policy.js.map +1 -1
- package/dist/src/runner/rework-policy.d.ts +33 -2
- package/dist/src/runner/rework-policy.d.ts.map +1 -1
- package/dist/src/runner/rework-policy.js +84 -14
- package/dist/src/runner/rework-policy.js.map +1 -1
- package/dist/src/runner/scoped-auto-command.d.ts +3 -1
- package/dist/src/runner/scoped-auto-command.d.ts.map +1 -1
- package/dist/src/runner/scoped-auto-command.js +52 -10
- package/dist/src/runner/scoped-auto-command.js.map +1 -1
- package/dist/src/runner/scoped-recovery.d.ts +13 -0
- package/dist/src/runner/scoped-recovery.d.ts.map +1 -1
- package/dist/src/runner/scoped-recovery.js +16 -14
- package/dist/src/runner/scoped-recovery.js.map +1 -1
- package/dist/src/setup/project-config.d.ts.map +1 -1
- package/dist/src/setup/project-config.js +14 -2
- package/dist/src/setup/project-config.js.map +1 -1
- package/dist/src/setup/setup-command.js +1 -0
- package/dist/src/setup/setup-command.js.map +1 -1
- package/docs/deep-dive.md +48 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { hostname as osHostname } from 'node:os';
|
|
2
3
|
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { CodexCommandAdapter } from '../codex/command-adapter.js';
|
|
4
5
|
import { resolveBaseBranch } from '../git/base-branch.js';
|
|
@@ -18,8 +19,9 @@ import { collectExecutableChildBatches, persistAutonomousChildNode, readAutonomo
|
|
|
18
19
|
import { runImplementationPublishabilityCheck } from './local-execution-session.js';
|
|
19
20
|
import { RunnerLifecycleEventStore } from './lifecycle-events.js';
|
|
20
21
|
import { RunnerStateStore } from './local-state.js';
|
|
22
|
+
import { classifyPlanAutoBlockedChildRecovery, classifyPlanAutoCompletedChildRecovery, classifyPlanAutoParentRecovery, } from './plan-auto-recovery.js';
|
|
21
23
|
import { buildIssueTreeChildPrompt, buildPlanAutoPrompt, sessionPromptPath, sessionReportPath, writeDurablePrompt, } from './prompt.js';
|
|
22
|
-
import {
|
|
24
|
+
import { decideImplementationRework } from './rework-policy.js';
|
|
23
25
|
import { evaluateParentRiskRoutingGate } from './review-gates.js';
|
|
24
26
|
import { buildBlockedHandoffEvidence, buildPromotionAsBlockedHandoffEvidence, buildReviewReadyHandoffEvidence, } from './runner-handoff-decision.js';
|
|
25
27
|
import { sessionLogPath } from './run-log.js';
|
|
@@ -28,11 +30,13 @@ class ChildExecutionBlockedError extends Error {
|
|
|
28
30
|
durableRunSummary;
|
|
29
31
|
freshContextReview;
|
|
30
32
|
acceptanceProof;
|
|
31
|
-
|
|
33
|
+
reworkAttempts;
|
|
34
|
+
constructor(message, durableRunSummary, freshContextReview, acceptanceProof, reworkAttempts) {
|
|
32
35
|
super(message);
|
|
33
36
|
this.durableRunSummary = durableRunSummary;
|
|
34
37
|
this.freshContextReview = freshContextReview;
|
|
35
38
|
this.acceptanceProof = acceptanceProof;
|
|
39
|
+
this.reworkAttempts = reworkAttempts;
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
export async function runPlanAutoCommand(options) {
|
|
@@ -49,16 +53,36 @@ export async function runPlanAutoCommand(options) {
|
|
|
49
53
|
if (!parentIssue) {
|
|
50
54
|
throw new Error(`Issue #${options.issueNumber} was not found`);
|
|
51
55
|
}
|
|
56
|
+
const branchName = renderBranchTemplate(config.branches.issueTree, { parentIssueNumber: options.issueNumber });
|
|
57
|
+
const worktreePath = join(targetRoot, config.runner.workspaceRoot, `tree-${options.issueNumber}`);
|
|
58
|
+
const store = new RunnerStateStore(targetRoot, config);
|
|
59
|
+
const parentRecovery = await classifyPlanAutoParentRecovery({
|
|
60
|
+
targetRoot,
|
|
61
|
+
config,
|
|
62
|
+
parentIssue,
|
|
63
|
+
branchName,
|
|
64
|
+
worktreePath,
|
|
65
|
+
baseSha: resolvedBase.sha,
|
|
66
|
+
state: await store.load(),
|
|
67
|
+
git,
|
|
68
|
+
now,
|
|
69
|
+
hostname: options.hostname,
|
|
70
|
+
processProbe: options.processProbe,
|
|
71
|
+
});
|
|
52
72
|
const decision = discoverIssueWork([parentIssue], config)[0];
|
|
53
|
-
|
|
73
|
+
const parentLabels = new Set(parentIssue.labels.map((label) => label.name));
|
|
74
|
+
const alreadyRunningPlanParent = decision?.kind === 'skipped'
|
|
75
|
+
&& decision.reasonCode === 'already-running'
|
|
76
|
+
&& parentIssue.state === 'OPEN'
|
|
77
|
+
&& parentLabels.has(config.github.labels.planAuto.name)
|
|
78
|
+
&& !parentLabels.has(config.github.labels.child.name);
|
|
79
|
+
if (!decision || !((decision.kind === 'eligible' && decision.mode === 'plan-parent') || alreadyRunningPlanParent)) {
|
|
54
80
|
const reason = decision?.kind === 'skipped' ? decision.reason : 'not agent:plan-auto';
|
|
55
81
|
throw new Error(`Issue #${options.issueNumber} is not eligible for agent:plan-auto planning: ${reason}`);
|
|
56
82
|
}
|
|
57
83
|
const workflowPrompts = await readPlanWorkflowPrompts(targetRoot, config);
|
|
58
84
|
const issueTreeWorkflowPrompt = await readIssueTreeWorkflowPrompt(targetRoot, config);
|
|
59
85
|
const acceptanceProofWorkflowPrompt = await readAcceptanceProofWorkflowPrompt(targetRoot, config);
|
|
60
|
-
const branchName = renderBranchTemplate(config.branches.issueTree, { parentIssueNumber: options.issueNumber });
|
|
61
|
-
const worktreePath = join(targetRoot, config.runner.workspaceRoot, `tree-${options.issueNumber}`);
|
|
62
86
|
let promptPath = '';
|
|
63
87
|
let reportPath = '';
|
|
64
88
|
let logPath = '';
|
|
@@ -66,19 +90,38 @@ export async function runPlanAutoCommand(options) {
|
|
|
66
90
|
let parentSnapshotPath = '';
|
|
67
91
|
const childIssues = [];
|
|
68
92
|
let pullRequest;
|
|
69
|
-
let store;
|
|
70
93
|
const executionResults = [];
|
|
71
94
|
const events = new RunnerLifecycleEventStore(targetRoot, config);
|
|
72
95
|
const buildBase = () => baseResult(options.issueNumber, branchName, worktreePath, promptPath, reportPath, logPath, childIssues, sessionId, parentSnapshotPath);
|
|
73
|
-
|
|
96
|
+
if (parentRecovery.kind === 'hard-block') {
|
|
97
|
+
return finishPlanBlocked(issueAdapter, config, events, buildBase(), [parentRecovery.reason], childIssues, parentRecovery.marker);
|
|
98
|
+
}
|
|
99
|
+
if (alreadyRunningPlanParent && parentRecovery.kind !== 'resume-parent') {
|
|
100
|
+
return finishPlanBlocked(issueAdapter, config, events, buildBase(), ['parent recovery requires runner-owned metadata for running parent'], childIssues, `plan-auto-recovery-blocked parent=${options.issueNumber} reason=missing-runner-owned-metadata`);
|
|
101
|
+
}
|
|
102
|
+
if (!alreadyRunningPlanParent) {
|
|
103
|
+
await claimIssue(issueAdapter, config, options.issueNumber, 'plan-parent', now);
|
|
104
|
+
}
|
|
74
105
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
if (parentRecovery.kind === 'resume-parent') {
|
|
107
|
+
await git.ensureIssueWorktree({
|
|
108
|
+
targetRoot,
|
|
109
|
+
workspacePath: worktreePath,
|
|
110
|
+
branchName,
|
|
111
|
+
baseBranch: resolvedBase.sha,
|
|
112
|
+
requiredBaseSha: resolvedBase.sha,
|
|
113
|
+
allowResume: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
await git.createIssueWorktree({
|
|
118
|
+
targetRoot,
|
|
119
|
+
workspacePath: worktreePath,
|
|
120
|
+
branchName,
|
|
121
|
+
baseBranch: resolvedBase.sha,
|
|
122
|
+
requiredBaseSha: resolvedBase.sha,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
82
125
|
sessionId = `plan-${options.issueNumber}-${formatSessionTimestamp(now)}`;
|
|
83
126
|
promptPath = sessionPromptPath({ targetRoot, config, issueNumber: options.issueNumber, sessionId });
|
|
84
127
|
reportPath = sessionReportPath({ targetRoot, config, issueNumber: options.issueNumber, sessionId });
|
|
@@ -130,7 +173,6 @@ export async function runPlanAutoCommand(options) {
|
|
|
130
173
|
summary: 'Starting parent planning Codex session.',
|
|
131
174
|
artifacts: sessionArtifacts(promptPath, reportPath, logPath, snapshot.path),
|
|
132
175
|
});
|
|
133
|
-
store = new RunnerStateStore(targetRoot, config);
|
|
134
176
|
await store.upsertRun({
|
|
135
177
|
issueNumber: options.issueNumber,
|
|
136
178
|
mode: 'plan-parent',
|
|
@@ -143,6 +185,10 @@ export async function runPlanAutoCommand(options) {
|
|
|
143
185
|
retryCount: 0,
|
|
144
186
|
createdAt: now.toISOString(),
|
|
145
187
|
updatedAt: now.toISOString(),
|
|
188
|
+
ownerPid: process.pid,
|
|
189
|
+
host: (options.hostname ?? osHostname)(),
|
|
190
|
+
leaseUpdatedAt: now.toISOString(),
|
|
191
|
+
baseSha: resolvedBase.sha,
|
|
146
192
|
});
|
|
147
193
|
const beforeHead = await git.getHead(worktreePath);
|
|
148
194
|
let codexResult;
|
|
@@ -196,7 +242,60 @@ export async function runPlanAutoCommand(options) {
|
|
|
196
242
|
childIssues.push(persisted);
|
|
197
243
|
}
|
|
198
244
|
const childNodes = await readAutonomousChildNodes(issueAdapter, config, options.issueNumber, childIssues);
|
|
199
|
-
const
|
|
245
|
+
const childRecoveryState = await store.load();
|
|
246
|
+
const childRecoveryDecisions = await Promise.all(childNodes.map(async (child) => {
|
|
247
|
+
const completed = await classifyPlanAutoCompletedChildRecovery({
|
|
248
|
+
targetRoot,
|
|
249
|
+
config,
|
|
250
|
+
parentIssueNumber: options.issueNumber,
|
|
251
|
+
parentBranchName: branchName,
|
|
252
|
+
child,
|
|
253
|
+
state: childRecoveryState,
|
|
254
|
+
git,
|
|
255
|
+
});
|
|
256
|
+
return completed.kind === 'execute-child'
|
|
257
|
+
? classifyPlanAutoBlockedChildRecovery({
|
|
258
|
+
targetRoot,
|
|
259
|
+
config,
|
|
260
|
+
parentIssueNumber: options.issueNumber,
|
|
261
|
+
child,
|
|
262
|
+
state: childRecoveryState,
|
|
263
|
+
git,
|
|
264
|
+
})
|
|
265
|
+
: completed;
|
|
266
|
+
}));
|
|
267
|
+
const recoveredStableIds = [];
|
|
268
|
+
const resumableBlockedStableIds = [];
|
|
269
|
+
const childRecoveryByStableId = new Map();
|
|
270
|
+
for (const decision of childRecoveryDecisions) {
|
|
271
|
+
if (decision.kind === 'hard-block') {
|
|
272
|
+
return finishPlanBlocked(issueAdapter, config, events, buildBase(), [decision.reason], childIssues, decision.marker);
|
|
273
|
+
}
|
|
274
|
+
if (decision.kind === 'recovered-completed-child') {
|
|
275
|
+
recoveredStableIds.push(decision.child.metadata.stableId);
|
|
276
|
+
executionResults.push({
|
|
277
|
+
child: decision.child,
|
|
278
|
+
branchName: decision.branchName,
|
|
279
|
+
worktreePath: decision.worktreePath,
|
|
280
|
+
promptPath: decision.promptPath,
|
|
281
|
+
reportPath: decision.reportPath,
|
|
282
|
+
logPath: decision.logPath,
|
|
283
|
+
changedFiles: decision.changedFiles,
|
|
284
|
+
validation: decision.validation,
|
|
285
|
+
artifacts: [],
|
|
286
|
+
commits: [],
|
|
287
|
+
skippedChecks: decision.skippedChecks,
|
|
288
|
+
residualRisks: decision.residualRisks,
|
|
289
|
+
durableRunSummary: decision.durableRunSummary,
|
|
290
|
+
recovered: true,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
else if (decision.kind === 'resume-child-rework') {
|
|
294
|
+
resumableBlockedStableIds.push(decision.child.metadata.stableId);
|
|
295
|
+
childRecoveryByStableId.set(decision.child.metadata.stableId, decision);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const batches = collectExecutableChildBatches(childNodes, config, recoveredStableIds, resumableBlockedStableIds);
|
|
200
299
|
if (!batches.ok) {
|
|
201
300
|
return finishPlanBlocked(issueAdapter, config, events, base, batches.errors, childIssues);
|
|
202
301
|
}
|
|
@@ -216,6 +315,7 @@ export async function runPlanAutoCommand(options) {
|
|
|
216
315
|
acceptanceProofWorkflowPrompt,
|
|
217
316
|
now,
|
|
218
317
|
events,
|
|
318
|
+
recovery: childRecoveryByStableId.get(child.metadata.stableId),
|
|
219
319
|
})));
|
|
220
320
|
const failures = [];
|
|
221
321
|
for (const [index, result] of settled.entries()) {
|
|
@@ -266,7 +366,7 @@ export async function runPlanAutoCommand(options) {
|
|
|
266
366
|
executionResults.push(childResult);
|
|
267
367
|
}
|
|
268
368
|
}
|
|
269
|
-
const finalValidation = await runConfiguredChecks(config, worktreePath, shellExecutor, []);
|
|
369
|
+
const finalValidation = await runConfiguredChecks(config, worktreePath, shellExecutor, [], { phase: 'parent-integration' });
|
|
270
370
|
const failedFinalValidation = finalValidation.filter((line) => line.status === 'failed');
|
|
271
371
|
if (failedFinalValidation.length > 0) {
|
|
272
372
|
await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, 'Parent integration configured checks failed before publication.', worktreePath);
|
|
@@ -331,7 +431,7 @@ export async function runPlanAutoCommand(options) {
|
|
|
331
431
|
if (pullRequest) {
|
|
332
432
|
throw new Error(`Issue-tree execution failed after draft PR creation (${pullRequest.url}); not marking parent blocked: ${message}`);
|
|
333
433
|
}
|
|
334
|
-
if (
|
|
434
|
+
if (executionResults.length > 0) {
|
|
335
435
|
await blockCompletedChildren(issueAdapter, config, store, options.issueNumber, executionResults, `Parent publication failed before review handoff: ${message}`, worktreePath);
|
|
336
436
|
}
|
|
337
437
|
return finishPlanBlocked(issueAdapter, config, events, buildBase(), [message], childIssues);
|
|
@@ -379,77 +479,76 @@ async function executeChild(input) {
|
|
|
379
479
|
const childIssueNumber = input.child.issue.number;
|
|
380
480
|
const branchName = `codex/tree-${input.parentIssue.number}-issue-${childIssueNumber}`;
|
|
381
481
|
const worktreePath = join(input.targetRoot, input.config.runner.workspaceRoot, `tree-${input.parentIssue.number}-issue-${childIssueNumber}`);
|
|
382
|
-
const sessionId = `tree-${input.parentIssue.number}-issue-${childIssueNumber}-${formatSessionTimestamp(input.now)}`;
|
|
383
|
-
const promptPath = sessionPromptPath({
|
|
384
|
-
targetRoot: input.targetRoot,
|
|
385
|
-
config: input.config,
|
|
386
|
-
issueNumber: childIssueNumber,
|
|
387
|
-
sessionId,
|
|
388
|
-
});
|
|
389
|
-
const reportPath = sessionReportPath({
|
|
390
|
-
targetRoot: input.targetRoot,
|
|
391
|
-
config: input.config,
|
|
392
|
-
issueNumber: childIssueNumber,
|
|
393
|
-
sessionId,
|
|
394
|
-
});
|
|
395
|
-
const logPath = sessionLogPath({
|
|
396
|
-
targetRoot: input.targetRoot,
|
|
397
|
-
config: input.config,
|
|
398
|
-
issueNumber: childIssueNumber,
|
|
399
|
-
sessionId,
|
|
400
|
-
});
|
|
401
|
-
const isolatedHomePath = sessionCodexHomePath({ targetRoot: input.targetRoot, sessionId });
|
|
402
482
|
const store = new RunnerStateStore(input.targetRoot, input.config);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
sessionId,
|
|
430
|
-
promptText,
|
|
431
|
-
});
|
|
432
|
-
await store.upsertRun({
|
|
433
|
-
issueNumber: childIssueNumber,
|
|
434
|
-
parentIssueNumber: input.parentIssue.number,
|
|
435
|
-
mode: 'tree-child',
|
|
436
|
-
workspacePath: worktreePath,
|
|
437
|
-
sessionId,
|
|
438
|
-
branchName,
|
|
439
|
-
promptPath,
|
|
440
|
-
reportPath,
|
|
441
|
-
logPath,
|
|
442
|
-
retryCount: 0,
|
|
443
|
-
createdAt: input.now.toISOString(),
|
|
444
|
-
updatedAt: input.now.toISOString(),
|
|
445
|
-
});
|
|
483
|
+
let sessionId = '';
|
|
484
|
+
let promptPath = '';
|
|
485
|
+
let reportPath = '';
|
|
486
|
+
let logPath = '';
|
|
487
|
+
let snapshotPath = '';
|
|
488
|
+
if (input.recovery) {
|
|
489
|
+
await input.git.ensureIssueWorktree({
|
|
490
|
+
targetRoot: input.targetRoot,
|
|
491
|
+
workspacePath: worktreePath,
|
|
492
|
+
branchName,
|
|
493
|
+
baseBranch: input.parentBranchName,
|
|
494
|
+
allowResume: true,
|
|
495
|
+
});
|
|
496
|
+
await input.issueAdapter.removeLabels(childIssueNumber, [input.config.github.labels.blocked.name]);
|
|
497
|
+
await input.issueAdapter.addLabels(childIssueNumber, [input.config.github.labels.running.name]);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
await input.git.createIssueWorktree({
|
|
501
|
+
targetRoot: input.targetRoot,
|
|
502
|
+
workspacePath: worktreePath,
|
|
503
|
+
branchName,
|
|
504
|
+
baseBranch: input.parentBranchName,
|
|
505
|
+
});
|
|
506
|
+
await input.issueAdapter.addLabels(childIssueNumber, [input.config.github.labels.running.name]);
|
|
507
|
+
await input.issueAdapter.postComment(childIssueNumber, `codex-orchestrator: claimed #${childIssueNumber} for tree-child autonomous work under #${input.parentIssue.number} at ${input.now.toISOString()}.`);
|
|
508
|
+
}
|
|
446
509
|
let publishability;
|
|
447
|
-
let rework;
|
|
510
|
+
let rework = input.recovery?.rework;
|
|
511
|
+
const reworkAttempts = [];
|
|
448
512
|
const maxReworkAttempts = Math.max(input.config.loopPolicy.rework.maxAttempts, input.config.reviewGates.acceptanceProof.maxIterations - 1);
|
|
449
|
-
|
|
513
|
+
const firstAttempt = input.recovery ? input.recovery.rework.attempt : 0;
|
|
514
|
+
for (let attempt = firstAttempt; attempt <= maxReworkAttempts; attempt++) {
|
|
450
515
|
const attemptNow = new Date(input.now.getTime() + attempt);
|
|
516
|
+
sessionId = `tree-${input.parentIssue.number}-issue-${childIssueNumber}-${formatSessionTimestamp(attemptNow)}${attempt === 0 ? '' : `-attempt-${attempt}`}`;
|
|
517
|
+
promptPath = sessionPromptPath({
|
|
518
|
+
targetRoot: input.targetRoot,
|
|
519
|
+
config: input.config,
|
|
520
|
+
issueNumber: childIssueNumber,
|
|
521
|
+
sessionId,
|
|
522
|
+
});
|
|
523
|
+
reportPath = sessionReportPath({
|
|
524
|
+
targetRoot: input.targetRoot,
|
|
525
|
+
config: input.config,
|
|
526
|
+
issueNumber: childIssueNumber,
|
|
527
|
+
sessionId,
|
|
528
|
+
});
|
|
529
|
+
logPath = sessionLogPath({
|
|
530
|
+
targetRoot: input.targetRoot,
|
|
531
|
+
config: input.config,
|
|
532
|
+
issueNumber: childIssueNumber,
|
|
533
|
+
sessionId,
|
|
534
|
+
});
|
|
535
|
+
const isolatedHomePath = sessionCodexHomePath({ targetRoot: input.targetRoot, sessionId });
|
|
536
|
+
await mkdir(dirname(reportPath), { recursive: true });
|
|
537
|
+
await mkdir(isolatedHomePath, { recursive: true });
|
|
538
|
+
const promptText = buildIssueTreeChildPrompt({
|
|
539
|
+
parentIssue: input.parentIssue,
|
|
540
|
+
childIssue: input.child.issue,
|
|
541
|
+
config: input.config,
|
|
542
|
+
workflowPromptText: input.issueTreeWorkflowPrompt,
|
|
543
|
+
childMetadata: input.child.metadata,
|
|
544
|
+
dependencyIssues: input.dependencyIssues,
|
|
545
|
+
promptPath,
|
|
546
|
+
reportPath,
|
|
547
|
+
branchName,
|
|
548
|
+
worktreePath,
|
|
549
|
+
});
|
|
451
550
|
const attemptPromptText = rework
|
|
452
|
-
? `${promptText}\n\n## Rework Request\nThis is an automatic rework attempt (#${rework.attempt}). Continue from the current worktree state; do not start over.\nThe previous attempt was blocked for these reasons:\n${rework.blockedReasons.map((reason) => `- ${reason}`).join('\n')}\
|
|
551
|
+
? `${promptText}\n\n## Rework Request\nThis is an automatic rework attempt (#${rework.attempt}). Continue from the current worktree state; do not start over.\nThe previous attempt was blocked for these reasons:\n${rework.blockedReasons.map((reason) => `- ${reason}`).join('\n')}\n${rework.disableOptionalFigmaMcp ? 'Optional Figma MCP failed in the previous attempt. Continue without optional Figma MCP access unless the issue explicitly requires Figma.\n' : ''}Address the blockers, then produce a fresh completion report JSON for the runner.`
|
|
453
552
|
: promptText;
|
|
454
553
|
await writeDurablePrompt({
|
|
455
554
|
targetRoot: input.targetRoot,
|
|
@@ -478,6 +577,23 @@ async function executeChild(input) {
|
|
|
478
577
|
blockedBy: [],
|
|
479
578
|
createdAt: attemptNow,
|
|
480
579
|
});
|
|
580
|
+
snapshotPath = snapshot.path;
|
|
581
|
+
await store.upsertRun({
|
|
582
|
+
issueNumber: childIssueNumber,
|
|
583
|
+
parentIssueNumber: input.parentIssue.number,
|
|
584
|
+
mode: 'tree-child',
|
|
585
|
+
workspacePath: worktreePath,
|
|
586
|
+
sessionId,
|
|
587
|
+
branchName,
|
|
588
|
+
promptPath,
|
|
589
|
+
reportPath,
|
|
590
|
+
logPath,
|
|
591
|
+
retryCount: attempt,
|
|
592
|
+
createdAt: input.now.toISOString(),
|
|
593
|
+
updatedAt: attemptNow.toISOString(),
|
|
594
|
+
attemptStartedAt: attemptNow.toISOString(),
|
|
595
|
+
snapshotPath,
|
|
596
|
+
});
|
|
481
597
|
await safeAppendEvent(input.events, {
|
|
482
598
|
timestamp: attemptNow,
|
|
483
599
|
issueNumber: childIssueNumber,
|
|
@@ -487,7 +603,7 @@ async function executeChild(input) {
|
|
|
487
603
|
phase: 'tree-child',
|
|
488
604
|
status: 'started',
|
|
489
605
|
summary: rework ? `Starting tree-child rework attempt #${rework.attempt}.` : 'Starting tree-child Codex session.',
|
|
490
|
-
artifacts: sessionArtifacts(promptPath, reportPath, logPath,
|
|
606
|
+
artifacts: sessionArtifacts(promptPath, reportPath, logPath, snapshotPath),
|
|
491
607
|
});
|
|
492
608
|
const beforeHead = await input.git.getHead(worktreePath);
|
|
493
609
|
let codexResult;
|
|
@@ -505,6 +621,7 @@ async function executeChild(input) {
|
|
|
505
621
|
branchName,
|
|
506
622
|
phase: 'tree-child',
|
|
507
623
|
logPath,
|
|
624
|
+
disableOptionalFigmaMcp: rework?.disableOptionalFigmaMcp,
|
|
508
625
|
});
|
|
509
626
|
}
|
|
510
627
|
finally {
|
|
@@ -538,12 +655,49 @@ async function executeChild(input) {
|
|
|
538
655
|
}),
|
|
539
656
|
},
|
|
540
657
|
});
|
|
541
|
-
if (publishability.status === 'blocked'
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
658
|
+
if (publishability.status === 'blocked') {
|
|
659
|
+
const reworkDecision = decideImplementationRework({
|
|
660
|
+
reasons: publishability.reasons,
|
|
661
|
+
config: input.config,
|
|
662
|
+
attempt,
|
|
663
|
+
});
|
|
664
|
+
reworkAttempts.push({
|
|
665
|
+
attempt,
|
|
666
|
+
maxAttempts: 'maxAttempts' in reworkDecision ? reworkDecision.maxAttempts : undefined,
|
|
667
|
+
decisionKind: reworkDecision.kind,
|
|
668
|
+
reasons: publishability.reasons,
|
|
669
|
+
promptPath,
|
|
670
|
+
reportPath,
|
|
671
|
+
logPath,
|
|
672
|
+
snapshotPath,
|
|
673
|
+
});
|
|
674
|
+
if (reworkDecision.kind === 'retry') {
|
|
675
|
+
rework = reworkDecision.rework;
|
|
676
|
+
await safeAppendEvent(input.events, {
|
|
677
|
+
timestamp: attemptNow,
|
|
678
|
+
issueNumber: childIssueNumber,
|
|
679
|
+
parentIssueNumber: input.parentIssue.number,
|
|
680
|
+
mode: 'tree-child',
|
|
681
|
+
sessionId,
|
|
682
|
+
phase: 'tree-child',
|
|
683
|
+
status: 'needs-rework',
|
|
684
|
+
summary: `Runner scheduled tree-child rework attempt #${reworkDecision.nextAttempt}.`,
|
|
685
|
+
artifacts: sessionArtifacts(promptPath, reportPath, logPath, snapshotPath),
|
|
686
|
+
});
|
|
687
|
+
await mkdir(isolatedHomePath, { recursive: true });
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
await safeAppendEvent(input.events, {
|
|
691
|
+
timestamp: attemptNow,
|
|
692
|
+
issueNumber: childIssueNumber,
|
|
693
|
+
parentIssueNumber: input.parentIssue.number,
|
|
694
|
+
mode: 'tree-child',
|
|
695
|
+
sessionId,
|
|
696
|
+
phase: 'tree-child',
|
|
697
|
+
status: 'blocked',
|
|
698
|
+
summary: `Runner rework decision: ${reworkDecision.kind}.`,
|
|
699
|
+
artifacts: sessionArtifacts(promptPath, reportPath, logPath, snapshotPath),
|
|
700
|
+
});
|
|
547
701
|
}
|
|
548
702
|
break;
|
|
549
703
|
}
|
|
@@ -572,13 +726,14 @@ async function executeChild(input) {
|
|
|
572
726
|
logPath,
|
|
573
727
|
reportPath,
|
|
574
728
|
acceptanceProof: evidence.acceptanceProof,
|
|
729
|
+
reworkAttempts,
|
|
575
730
|
});
|
|
576
731
|
const message = [
|
|
577
732
|
'Child requested promotion instead of completing issue-tree work.',
|
|
578
733
|
promotion ? `Reason: ${promotion.reason}` : undefined,
|
|
579
734
|
promotion ? `Evidence: ${promotion.evidence.join(', ')}` : undefined,
|
|
580
735
|
].filter(Boolean).join(' ');
|
|
581
|
-
throw new ChildExecutionBlockedError(message, durableRunSummary, undefined, evidence.acceptanceProof);
|
|
736
|
+
throw new ChildExecutionBlockedError(message, durableRunSummary, undefined, evidence.acceptanceProof, reworkAttempts);
|
|
582
737
|
}
|
|
583
738
|
if (publishability.status === 'blocked') {
|
|
584
739
|
const evidence = buildBlockedHandoffEvidence({
|
|
@@ -600,8 +755,9 @@ async function executeChild(input) {
|
|
|
600
755
|
logPath,
|
|
601
756
|
reportPath,
|
|
602
757
|
acceptanceProof: evidence.acceptanceProof,
|
|
758
|
+
reworkAttempts,
|
|
603
759
|
});
|
|
604
|
-
throw new ChildExecutionBlockedError(evidence.blockers.join('; '), durableRunSummary, undefined, evidence.acceptanceProof);
|
|
760
|
+
throw new ChildExecutionBlockedError(evidence.blockers.join('; '), durableRunSummary, undefined, evidence.acceptanceProof, reworkAttempts);
|
|
605
761
|
}
|
|
606
762
|
const freshContextReview = await runFreshContextReviewIfEnabled({
|
|
607
763
|
targetRoot: input.targetRoot,
|
|
@@ -635,8 +791,9 @@ async function executeChild(input) {
|
|
|
635
791
|
logPath,
|
|
636
792
|
reportPath,
|
|
637
793
|
acceptanceProof: evidence.acceptanceProof,
|
|
794
|
+
reworkAttempts,
|
|
638
795
|
});
|
|
639
|
-
throw new ChildExecutionBlockedError(evidence.blockers.join('; '), durableRunSummary, freshContextReview, evidence.acceptanceProof);
|
|
796
|
+
throw new ChildExecutionBlockedError(evidence.blockers.join('; '), durableRunSummary, freshContextReview, evidence.acceptanceProof, reworkAttempts);
|
|
640
797
|
}
|
|
641
798
|
const evidence = buildReviewReadyHandoffEvidence({
|
|
642
799
|
publishability,
|
|
@@ -659,6 +816,7 @@ async function executeChild(input) {
|
|
|
659
816
|
logPath,
|
|
660
817
|
reportPath,
|
|
661
818
|
acceptanceProof: evidence.acceptanceProof,
|
|
819
|
+
reworkAttempts,
|
|
662
820
|
});
|
|
663
821
|
return {
|
|
664
822
|
child: input.child,
|
|
@@ -693,6 +851,7 @@ async function blockFailedBatch(issueAdapter, config, parentIssueNumber, failure
|
|
|
693
851
|
details: ['Worktree preserved for maintainer inspection.'],
|
|
694
852
|
freshContextReview: failure.freshContextReview,
|
|
695
853
|
durableRunSummary: failure.durableRunSummary,
|
|
854
|
+
reworkAttempts: failure.reworkAttempts,
|
|
696
855
|
acceptanceProof: failure.acceptanceProof,
|
|
697
856
|
}));
|
|
698
857
|
}
|
|
@@ -747,6 +906,10 @@ async function handleMergeConflict(git, issueAdapter, config, parentIssueNumber,
|
|
|
747
906
|
}
|
|
748
907
|
async function markCompletedChildrenReviewReady(issueAdapter, config, store, parentIssueNumber, childResults) {
|
|
749
908
|
for (const childResult of childResults) {
|
|
909
|
+
if (childResult.recovered) {
|
|
910
|
+
await store.removeRun(childResult.child.issue.number);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
750
913
|
await issueAdapter.removeLabels(childResult.child.issue.number, [config.github.labels.running.name]);
|
|
751
914
|
await issueAdapter.addLabels(childResult.child.issue.number, [config.github.labels.review.name]);
|
|
752
915
|
await issueAdapter.postComment(childResult.child.issue.number, buildChildReviewReport({ parentIssueNumber, result: childResult }));
|
|
@@ -755,6 +918,9 @@ async function markCompletedChildrenReviewReady(issueAdapter, config, store, par
|
|
|
755
918
|
}
|
|
756
919
|
async function blockCompletedChildren(issueAdapter, config, store, parentIssueNumber, childResults, reason, parentWorktreePath) {
|
|
757
920
|
for (const result of childResults) {
|
|
921
|
+
if (result.recovered) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
758
924
|
await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
|
|
759
925
|
await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
|
|
760
926
|
await issueAdapter.postComment(result.child.issue.number, buildChildBlockedReport({
|
|
@@ -771,16 +937,20 @@ async function blockCompletedChildren(issueAdapter, config, store, parentIssueNu
|
|
|
771
937
|
await store.removeRun(result.child.issue.number);
|
|
772
938
|
}
|
|
773
939
|
}
|
|
774
|
-
async function finishPlanBlocked(issueAdapter, config, events, result, reasons, mutatedChildren) {
|
|
775
|
-
const
|
|
940
|
+
async function finishPlanBlocked(issueAdapter, config, events, result, reasons, mutatedChildren, commentMarker) {
|
|
941
|
+
const reportBody = buildParentBlockedReport({
|
|
776
942
|
parentIssueNumber: result.parentIssueNumber,
|
|
777
943
|
reasons,
|
|
778
944
|
logPath: result.logPath,
|
|
779
945
|
mutatedChildren,
|
|
780
946
|
});
|
|
947
|
+
const reportComment = commentMarker ? `<!-- codex-orchestrator:${commentMarker} -->\n${reportBody}` : reportBody;
|
|
781
948
|
await issueAdapter.removeLabels(result.parentIssueNumber, [config.github.labels.running.name]);
|
|
782
949
|
await issueAdapter.addLabels(result.parentIssueNumber, [config.github.labels.blocked.name]);
|
|
783
|
-
await issueAdapter.
|
|
950
|
+
const currentIssue = commentMarker ? await issueAdapter.getIssue(result.parentIssueNumber) : undefined;
|
|
951
|
+
if (!commentMarker || !currentIssue?.comments.some((comment) => comment.body.includes(commentMarker))) {
|
|
952
|
+
await issueAdapter.postComment(result.parentIssueNumber, reportComment);
|
|
953
|
+
}
|
|
784
954
|
await safeAppendEvent(events, {
|
|
785
955
|
issueNumber: result.parentIssueNumber,
|
|
786
956
|
mode: 'plan-parent',
|