codex-orchestrator 2.0.4 → 2.0.5
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 +29 -3
- package/dist/src/v2/acceptance-proof.d.ts +5 -0
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +10 -2
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts +7 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts.map +1 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.js +288 -0
- package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
- package/dist/src/v2/adapters/pull-requests.d.ts +69 -0
- package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
- package/dist/src/v2/adapters/pull-requests.js +48 -0
- package/dist/src/v2/adapters/pull-requests.js.map +1 -1
- package/dist/src/v2/adapters/worktree.d.ts +1 -0
- package/dist/src/v2/adapters/worktree.d.ts.map +1 -1
- package/dist/src/v2/adapters/worktree.js +10 -1
- package/dist/src/v2/adapters/worktree.js.map +1 -1
- package/dist/src/v2/cli.d.ts +9 -0
- package/dist/src/v2/cli.d.ts.map +1 -1
- package/dist/src/v2/cli.js +38 -11
- package/dist/src/v2/cli.js.map +1 -1
- package/dist/src/v2/config.d.ts +0 -1
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +0 -3
- package/dist/src/v2/config.js.map +1 -1
- package/dist/src/v2/containment.d.ts +11 -2
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +35 -7
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +1 -1
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +9 -3
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts +54 -0
- package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -0
- package/dist/src/v2/review-feedback-coordinator.js +245 -0
- package/dist/src/v2/review-feedback-coordinator.js.map +1 -0
- package/dist/src/v2/review-feedback.d.ts +127 -0
- package/dist/src/v2/review-feedback.d.ts.map +1 -0
- package/dist/src/v2/review-feedback.js +436 -0
- package/dist/src/v2/review-feedback.js.map +1 -0
- package/dist/src/v2/run-issue.d.ts +57 -0
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +701 -36
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +45 -1
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +113 -4
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +37 -0
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +120 -5
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/setup.js +1 -1
- package/dist/src/v2/setup.js.map +1 -1
- package/docs/deep-dive.md +61 -5
- package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
- package/internal-workflow/evals/coding-skill-evals.json +18 -0
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/skills/code-debugger/SKILL.md +3 -3
- package/internal-workflow/skills/code-review/SKILL.md +18 -6
- package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
- package/internal-workflow/skills/spec-implementer/references/review-loop.md +7 -1
- package/internal-workflow/skills/tdd/SKILL.md +5 -4
- package/internal-workflow/skills/tdd/evals/evals.json +18 -0
- package/internal-workflow/skills/tdd/mocking.md +3 -42
- package/internal-workflow/skills/tdd/refactoring.md +6 -8
- package/package.json +1 -1
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -4,10 +4,11 @@ import { parseAgentAutoConfig } from './config.js';
|
|
|
4
4
|
import { canonicalJson, sha256 } from './containment.js';
|
|
5
5
|
import { validateImplementationReport } from './implementation-report.js';
|
|
6
6
|
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
|
|
7
|
-
import { ProofQuiescenceError } from './acceptance-proof.js';
|
|
7
|
+
import { ProofLaunchAuthorizationError, ProofQuiescenceError } from './acceptance-proof.js';
|
|
8
8
|
import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
|
|
9
9
|
import { initialRouteExecution, } from './route-coordinator.js';
|
|
10
10
|
import { downstreamLifecycleForRoute, validateRouteTransition, validateTrustedAnswerResumeTransition, } from './route-decision.js';
|
|
11
|
+
import { activateReviewFeedback, blockReviewFeedback, bootstrapReviewFeedback, markReviewFeedbackPublishing, markReviewFeedbackRepairing, markReviewFeedbackVerified, projectReviewFeedbackBatch, publishReviewFeedback, reserveNextReviewFeedbackRound, } from './review-feedback.js';
|
|
11
12
|
export class OwnerLockSafetyError extends Error {
|
|
12
13
|
}
|
|
13
14
|
export class OwnerLockContentionError extends Error {
|
|
@@ -249,10 +250,17 @@ export class RunIssue {
|
|
|
249
250
|
return await this.publicationDiverged(active, 'terminal-labels-diverged');
|
|
250
251
|
}
|
|
251
252
|
const evidence = await this.dependencies.writeEvidence({ runId, code: 'review-ready', summary: pullRequest.url });
|
|
252
|
-
const outcome = { status: 'review-ready', pullRequestUrl: pullRequest.url, evidencePath: evidence.path };
|
|
253
|
+
const outcome = { status: 'review-ready', pullRequestUrl: pullRequest.url, evidencePath: evidence.path, continuationEpoch: commitSha };
|
|
253
254
|
try {
|
|
254
255
|
await this.persist(active, {
|
|
255
256
|
lifecycle: 'review-ready', intent: undefined, outcomeEvidenceId: evidence.id, terminalOutcome: outcome,
|
|
257
|
+
...(active.record.routeReceipt?.route === 'direct' ? {
|
|
258
|
+
reviewFeedback: bootstrapReviewFeedback(active.record.reviewFeedback ?? {
|
|
259
|
+
version: 1, phase: 'bootstrap-required', consumedSourceIds: [], previousPublishedHeadSha: null,
|
|
260
|
+
repairRound: 0, activeBatch: null, implementationInvocation: null, history: [],
|
|
261
|
+
verifiedReceipt: null, terminal: null,
|
|
262
|
+
}, commitSha, []),
|
|
263
|
+
} : {}),
|
|
256
264
|
...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'review-ready' }) } : {}),
|
|
257
265
|
});
|
|
258
266
|
}
|
|
@@ -264,9 +272,409 @@ export class RunIssue {
|
|
|
264
272
|
publicationDiverged(active, code) {
|
|
265
273
|
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, code);
|
|
266
274
|
}
|
|
275
|
+
async updateExistingPullRequest(starting, config, issueNumber) {
|
|
276
|
+
let active = starting;
|
|
277
|
+
const feedback = active.record.reviewFeedback;
|
|
278
|
+
const batch = feedback?.activeBatch;
|
|
279
|
+
const coordinator = this.dependencies.reviewFeedback;
|
|
280
|
+
if (!feedback || feedback.phase !== 'publishing' || !batch || !coordinator
|
|
281
|
+
|| !this.dependencies.pullRequests.listConversationComments || !this.dependencies.pullRequests.postConversationComment) {
|
|
282
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-contract-missing');
|
|
283
|
+
}
|
|
284
|
+
const oldHead = batch.priorPublishedHeadSha;
|
|
285
|
+
const message = `fix: address review feedback for #${issueNumber}`;
|
|
286
|
+
let head = await this.dependencies.git.getHead(active.record.worktreePath);
|
|
287
|
+
let remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
|
|
288
|
+
if (!active.record.intent) {
|
|
289
|
+
if (head !== oldHead || remote !== oldHead) {
|
|
290
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-update-baseline-diverged');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!active.record.intent || active.record.intent.kind === 'review-update-commit') {
|
|
294
|
+
if (!active.record.intent) {
|
|
295
|
+
active = await this.persist(active, { intent: {
|
|
296
|
+
kind: 'review-update-commit', batchId: batch.batchId, parentSha: oldHead,
|
|
297
|
+
treeSha: await this.dependencies.git.getTreeSha(active.record.worktreePath), message,
|
|
298
|
+
} });
|
|
299
|
+
}
|
|
300
|
+
const intent = active.record.intent;
|
|
301
|
+
if (intent?.kind !== 'review-update-commit' || intent.batchId !== batch.batchId
|
|
302
|
+
|| intent.parentSha !== oldHead || intent.message !== message) {
|
|
303
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-intent-diverged');
|
|
304
|
+
}
|
|
305
|
+
head = await this.dependencies.git.getHead(active.record.worktreePath);
|
|
306
|
+
if (head === intent.parentSha) {
|
|
307
|
+
if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
|
|
308
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
309
|
+
}
|
|
310
|
+
const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
311
|
+
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-precommit-revalidation-failed');
|
|
312
|
+
if (validationFailure)
|
|
313
|
+
return validationFailure;
|
|
314
|
+
if (await this.dependencies.git.getTreeSha(active.record.worktreePath) !== intent.treeSha) {
|
|
315
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-tree-diverged');
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
head = await this.dependencies.git.commit({ worktreePath: active.record.worktreePath, message: intent.message });
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return this.invokedFailure(active, 'review-feedback-commit-delivery-unknown');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const commit = await this.dependencies.git.inspectHead(active.record.worktreePath);
|
|
325
|
+
if (commit.sha !== head || commit.parentSha !== intent.parentSha || commit.treeSha !== intent.treeSha || commit.message !== intent.message) {
|
|
326
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-observation-diverged');
|
|
327
|
+
}
|
|
328
|
+
active = await this.persist(active, { intent: {
|
|
329
|
+
kind: 'review-update-push', batchId: batch.batchId, branch: active.record.branchName,
|
|
330
|
+
priorRemoteSha: oldHead, sha: commit.sha, treeSha: commit.treeSha,
|
|
331
|
+
} });
|
|
332
|
+
}
|
|
333
|
+
const commit = await this.dependencies.git.inspectHead(active.record.worktreePath);
|
|
334
|
+
const recordedCommit = active.record.intent?.kind === 'review-update-push' ? active.record.intent : undefined;
|
|
335
|
+
if (commit.parentSha !== oldHead || commit.message !== message
|
|
336
|
+
|| (recordedCommit && (commit.sha !== recordedCommit.sha || commit.treeSha !== recordedCommit.treeSha))) {
|
|
337
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-update-commit-identity-diverged');
|
|
338
|
+
}
|
|
339
|
+
head = commit.sha;
|
|
340
|
+
if (!active.record.intent || active.record.intent.kind === 'review-update-push') {
|
|
341
|
+
if (!active.record.intent)
|
|
342
|
+
active = await this.persist(active, { intent: {
|
|
343
|
+
kind: 'review-update-push', batchId: batch.batchId, branch: active.record.branchName,
|
|
344
|
+
priorRemoteSha: oldHead, sha: head, treeSha: commit.treeSha,
|
|
345
|
+
} });
|
|
346
|
+
const intent = active.record.intent;
|
|
347
|
+
if (intent?.kind !== 'review-update-push' || intent.batchId !== batch.batchId || intent.sha !== head
|
|
348
|
+
|| intent.treeSha !== commit.treeSha || intent.branch !== active.record.branchName || intent.priorRemoteSha !== oldHead) {
|
|
349
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-push-intent-diverged');
|
|
350
|
+
}
|
|
351
|
+
remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
|
|
352
|
+
if (remote === oldHead) {
|
|
353
|
+
if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
|
|
354
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
355
|
+
}
|
|
356
|
+
const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
357
|
+
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-prepush-revalidation-failed');
|
|
358
|
+
if (validationFailure)
|
|
359
|
+
return validationFailure;
|
|
360
|
+
try {
|
|
361
|
+
await this.dependencies.git.push({ worktreePath: active.record.worktreePath, branchName: active.record.branchName });
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return this.invokedFailure(active, 'review-feedback-push-delivery-unknown');
|
|
365
|
+
}
|
|
366
|
+
remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
|
|
367
|
+
}
|
|
368
|
+
if (remote !== head)
|
|
369
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-push-observation-diverged');
|
|
370
|
+
}
|
|
371
|
+
const postPush = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
|
|
372
|
+
const postPushFailure = await this.mapFeedbackRevalidation(active, postPush, 'review-feedback-postpush-revalidation-failed');
|
|
373
|
+
if (postPushFailure)
|
|
374
|
+
return postPushFailure;
|
|
375
|
+
const marker = `<!-- codex-orchestrator:run:${active.record.runId}:review-feedback:${batch.batchId} -->`;
|
|
376
|
+
const body = [
|
|
377
|
+
marker,
|
|
378
|
+
'',
|
|
379
|
+
`Addressed frozen review feedback batch ${batch.batchId}.`,
|
|
380
|
+
`Updated head: ${head}`,
|
|
381
|
+
'Affected Closure, configured checks, and Acceptance Proof passed.',
|
|
382
|
+
'Review threads remain for human resolution.',
|
|
383
|
+
].join('\n');
|
|
384
|
+
if (active.record.intent?.kind === 'review-update-push') {
|
|
385
|
+
active = await this.persist(active, { intent: {
|
|
386
|
+
kind: 'review-summary', batchId: batch.batchId, pullRequestNumber: batch.pullRequest.number,
|
|
387
|
+
pullRequestNodeId: batch.pullRequest.nodeId, marker, bodySha256: sha256(body), epochHeadSha: head,
|
|
388
|
+
} });
|
|
389
|
+
}
|
|
390
|
+
let summaryId = '';
|
|
391
|
+
if (!active.record.intent || active.record.intent.kind === 'review-summary') {
|
|
392
|
+
if (!active.record.intent)
|
|
393
|
+
active = await this.persist(active, { intent: {
|
|
394
|
+
kind: 'review-summary', batchId: batch.batchId, pullRequestNumber: batch.pullRequest.number,
|
|
395
|
+
pullRequestNodeId: batch.pullRequest.nodeId, marker, bodySha256: sha256(body), epochHeadSha: head,
|
|
396
|
+
} });
|
|
397
|
+
const intent = active.record.intent;
|
|
398
|
+
if (intent?.kind !== 'review-summary' || intent.batchId !== batch.batchId
|
|
399
|
+
|| intent.pullRequestNumber !== batch.pullRequest.number || intent.pullRequestNodeId !== batch.pullRequest.nodeId
|
|
400
|
+
|| intent.marker !== marker || intent.bodySha256 !== sha256(body) || intent.epochHeadSha !== head) {
|
|
401
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-intent-diverged');
|
|
402
|
+
}
|
|
403
|
+
const validation = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
|
|
404
|
+
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-summary-revalidation-failed');
|
|
405
|
+
if (validationFailure)
|
|
406
|
+
return validationFailure;
|
|
407
|
+
let matches = (await this.dependencies.pullRequests.listConversationComments(batch.pullRequest.number))
|
|
408
|
+
.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
409
|
+
if (matches.some((comment) => comment.body !== body) || matches.length > 1) {
|
|
410
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-diverged');
|
|
411
|
+
}
|
|
412
|
+
if (matches.length === 0) {
|
|
413
|
+
if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
|
|
414
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
await this.dependencies.pullRequests.postConversationComment(batch.pullRequest.number, body);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return this.invokedFailure(active, 'review-feedback-summary-delivery-unknown');
|
|
421
|
+
}
|
|
422
|
+
matches = (await this.dependencies.pullRequests.listConversationComments(batch.pullRequest.number))
|
|
423
|
+
.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
424
|
+
}
|
|
425
|
+
if (matches.length !== 1 || matches[0].body !== body)
|
|
426
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-observation-diverged');
|
|
427
|
+
summaryId = matches[0].id;
|
|
428
|
+
active = await this.persist(active, { intent: {
|
|
429
|
+
kind: 'review-final-labels', issueNumber, batchId: batch.batchId,
|
|
430
|
+
pullRequestNumber: batch.pullRequest.number, pullRequestNodeId: batch.pullRequest.nodeId,
|
|
431
|
+
epochHeadSha: head, expected: [config.github.labels.review.name],
|
|
432
|
+
} });
|
|
433
|
+
}
|
|
434
|
+
if (!summaryId) {
|
|
435
|
+
const matches = (await this.dependencies.pullRequests.listConversationComments(batch.pullRequest.number))
|
|
436
|
+
.filter((comment) => comment.body.split('\n')[0] === marker && comment.body === body);
|
|
437
|
+
if (matches.length !== 1)
|
|
438
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-missing');
|
|
439
|
+
summaryId = matches[0].id;
|
|
440
|
+
}
|
|
441
|
+
const finalLabels = [config.github.labels.review.name];
|
|
442
|
+
if (!active.record.intent || active.record.intent.kind === 'review-final-labels') {
|
|
443
|
+
if (!active.record.intent)
|
|
444
|
+
active = await this.persist(active, { intent: {
|
|
445
|
+
kind: 'review-final-labels', issueNumber, batchId: batch.batchId,
|
|
446
|
+
pullRequestNumber: batch.pullRequest.number, pullRequestNodeId: batch.pullRequest.nodeId,
|
|
447
|
+
epochHeadSha: head, expected: finalLabels,
|
|
448
|
+
} });
|
|
449
|
+
const intent = active.record.intent;
|
|
450
|
+
if (intent?.kind !== 'review-final-labels' || intent.issueNumber !== issueNumber
|
|
451
|
+
|| intent.batchId !== batch.batchId || intent.pullRequestNumber !== batch.pullRequest.number
|
|
452
|
+
|| intent.pullRequestNodeId !== batch.pullRequest.nodeId || intent.epochHeadSha !== head
|
|
453
|
+
|| !sameStrings(intent.expected, finalLabels)) {
|
|
454
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-final-labels-intent-diverged');
|
|
455
|
+
}
|
|
456
|
+
const validation = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
|
|
457
|
+
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-final-labels-revalidation-failed');
|
|
458
|
+
if (validationFailure)
|
|
459
|
+
return validationFailure;
|
|
460
|
+
let issue = await this.readIssue(issueNumber);
|
|
461
|
+
if (!issue || !sameStrings(issue.labels, finalLabels)) {
|
|
462
|
+
if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
|
|
463
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
await this.dependencies.issues.setLabels(issueNumber, finalLabels);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return this.invokedFailure(active, 'review-feedback-final-labels-delivery-unknown');
|
|
470
|
+
}
|
|
471
|
+
issue = await this.readIssue(issueNumber);
|
|
472
|
+
}
|
|
473
|
+
if (!issue || !sameStrings(issue.labels, finalLabels))
|
|
474
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-final-labels-diverged');
|
|
475
|
+
}
|
|
476
|
+
const receipt = {
|
|
477
|
+
kind: 'published',
|
|
478
|
+
batchId: batch.batchId,
|
|
479
|
+
sourceIds: batch.sources.map((source) => source.sourceId),
|
|
480
|
+
priorHeadSha: oldHead,
|
|
481
|
+
publishedHeadSha: head,
|
|
482
|
+
pullRequestNumber: batch.pullRequest.number,
|
|
483
|
+
summaryCommentId: summaryId,
|
|
484
|
+
publishedAt: this.timestamp(),
|
|
485
|
+
};
|
|
486
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'review-ready', summary: active.record.terminalOutcome?.status === 'review-ready' ? active.record.terminalOutcome.pullRequestUrl : `PR #${batch.pullRequest.number}` });
|
|
487
|
+
const pullRequestUrl = `https://github.com/${active.record.canonicalRepository}/pull/${batch.pullRequest.number}`;
|
|
488
|
+
const outcome = { status: 'review-ready', pullRequestUrl, evidencePath: evidence.path, continuationEpoch: head };
|
|
489
|
+
try {
|
|
490
|
+
await this.persist(active, {
|
|
491
|
+
lifecycle: 'review-ready',
|
|
492
|
+
reviewFeedback: publishReviewFeedback(active.record.reviewFeedback, receipt),
|
|
493
|
+
terminalOutcome: outcome,
|
|
494
|
+
outcomeEvidenceId: evidence.id,
|
|
495
|
+
intent: undefined,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
throw new PostEffectStateError(active);
|
|
500
|
+
}
|
|
501
|
+
return publicOutcome(outcome);
|
|
502
|
+
}
|
|
503
|
+
async continueReviewReady(starting, targetRoot, config) {
|
|
504
|
+
const terminal = starting.record.terminalOutcome;
|
|
505
|
+
const feedback = starting.record.reviewFeedback;
|
|
506
|
+
if (!terminal || terminal.status !== 'review-ready' || !feedback || !this.dependencies.reviewFeedback
|
|
507
|
+
|| starting.record.routeReceipt?.route !== 'direct' || starting.record.directReview?.status !== 'clear') {
|
|
508
|
+
return { result: publicOutcome(terminal) };
|
|
509
|
+
}
|
|
510
|
+
const pullRequest = await this.dependencies.pullRequests.findOpen({
|
|
511
|
+
headBranch: starting.record.branchName,
|
|
512
|
+
baseBranch: config.github.baseBranch,
|
|
513
|
+
});
|
|
514
|
+
if (!pullRequest?.number || !pullRequest.nodeId || !pullRequest.headSha) {
|
|
515
|
+
return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-pr-identity-missing') };
|
|
516
|
+
}
|
|
517
|
+
const expectedHeadSha = feedback.phase === 'bootstrap-required'
|
|
518
|
+
? pullRequest.headSha
|
|
519
|
+
: feedback.previousPublishedHeadSha;
|
|
520
|
+
if (!expectedHeadSha) {
|
|
521
|
+
return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-head-missing') };
|
|
522
|
+
}
|
|
523
|
+
const observed = await this.dependencies.reviewFeedback.observeAndFreeze({
|
|
524
|
+
runId: starting.record.runId,
|
|
525
|
+
canonicalRepository: starting.record.canonicalRepository,
|
|
526
|
+
pullRequestNumber: pullRequest.number,
|
|
527
|
+
expectedHeadSha,
|
|
528
|
+
expectedHeadRefName: starting.record.branchName,
|
|
529
|
+
expectedBaseRefName: config.github.baseBranch,
|
|
530
|
+
marker: `<!-- codex-orchestrator:run:${starting.record.runId}:pr -->`,
|
|
531
|
+
consumedSourceIds: feedback.consumedSourceIds,
|
|
532
|
+
});
|
|
533
|
+
if (observed.status === 'retryable')
|
|
534
|
+
return { result: await this.invokedFailure(starting, 'review-feedback-observation-retryable') };
|
|
535
|
+
if (observed.status === 'blocked') {
|
|
536
|
+
return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-observation-blocked') };
|
|
537
|
+
}
|
|
538
|
+
if (feedback.phase === 'bootstrap-required') {
|
|
539
|
+
const sourceIds = observed.status === 'frozen'
|
|
540
|
+
? observed.batch.sources.map((source) => source.sourceId)
|
|
541
|
+
: observed.eligibleSourceIds;
|
|
542
|
+
const active = await this.persist(starting, {
|
|
543
|
+
reviewFeedback: bootstrapReviewFeedback(feedback, expectedHeadSha, sourceIds),
|
|
544
|
+
});
|
|
545
|
+
return { result: publicOutcome(active.record.terminalOutcome) };
|
|
546
|
+
}
|
|
547
|
+
if (feedback.phase !== 'idle' || observed.status === 'none')
|
|
548
|
+
return { result: publicOutcome(terminal) };
|
|
549
|
+
if (!await this.authorizedForExactLabels(starting.record.issueNumber, starting.record.runId, starting.record.branchName, [config.github.labels.review.name])) {
|
|
550
|
+
return { result: publicOutcome(terminal) };
|
|
551
|
+
}
|
|
552
|
+
const projected = projectReviewFeedbackBatch(observed.batch, starting.record.directReview.targetRevision);
|
|
553
|
+
const repairReview = beginDirectReviewRepair(starting.record.directReview, projected.repairFindings);
|
|
554
|
+
let active = await this.persist(starting, {
|
|
555
|
+
lifecycle: 'implementing',
|
|
556
|
+
reviewFeedback: activateReviewFeedback(feedback, observed.batch),
|
|
557
|
+
directReview: {
|
|
558
|
+
...repairReview,
|
|
559
|
+
review: { ...repairReview.review, reportRepairs: 0, transportRetries: 0 },
|
|
560
|
+
},
|
|
561
|
+
reworkFindings: projected.repairFindings.map((finding) => finding.summary),
|
|
562
|
+
reportRepairs: 0,
|
|
563
|
+
transportRetries: 0,
|
|
564
|
+
checks: [],
|
|
565
|
+
checkedChangeSha256: undefined,
|
|
566
|
+
proofId: undefined,
|
|
567
|
+
proofReceipt: undefined,
|
|
568
|
+
terminalOutcome: undefined,
|
|
569
|
+
outcomeEvidenceId: undefined,
|
|
570
|
+
intent: {
|
|
571
|
+
kind: 'review-activation-labels',
|
|
572
|
+
issueNumber: starting.record.issueNumber,
|
|
573
|
+
batchId: observed.batch.batchId,
|
|
574
|
+
expected: sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]),
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
return this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
578
|
+
}
|
|
579
|
+
async resumeActivatedReviewFeedback(starting, targetRoot, config) {
|
|
580
|
+
let active = starting;
|
|
581
|
+
const feedback = active.record.reviewFeedback;
|
|
582
|
+
const batch = feedback?.activeBatch;
|
|
583
|
+
if (!feedback || feedback.phase !== 'frozen' || !batch || !this.dependencies.reviewFeedback) {
|
|
584
|
+
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-state-invalid') };
|
|
585
|
+
}
|
|
586
|
+
const expectedHeadSha = batch.priorPublishedHeadSha;
|
|
587
|
+
const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
|
|
588
|
+
const reviewLabels = [config.github.labels.review.name];
|
|
589
|
+
const intent = active.record.intent;
|
|
590
|
+
if (intent && (intent.kind !== 'review-activation-labels'
|
|
591
|
+
|| intent.issueNumber !== active.record.issueNumber
|
|
592
|
+
|| intent.batchId !== batch.batchId
|
|
593
|
+
|| !sameStrings(intent.expected, runningLabels))) {
|
|
594
|
+
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-intent-diverged') };
|
|
595
|
+
}
|
|
596
|
+
const issue = await this.readIssue(active.record.issueNumber);
|
|
597
|
+
const labelsAreRunning = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, runningLabels)
|
|
598
|
+
&& this.hasTrustedClaim(issue, active.record.runId, active.record.issueNumber, active.record.branchName);
|
|
599
|
+
const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, reviewLabels)
|
|
600
|
+
&& this.hasTrustedClaim(issue, active.record.runId, active.record.issueNumber, active.record.branchName);
|
|
601
|
+
if (!labelsAreRunning && !labelsAreReview) {
|
|
602
|
+
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-authority-revoked') };
|
|
603
|
+
}
|
|
604
|
+
const validation = await this.dependencies.reviewFeedback.revalidate({
|
|
605
|
+
batch, epoch: 'pre-update', expectedHeadSha,
|
|
606
|
+
});
|
|
607
|
+
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-activation-revalidation-failed');
|
|
608
|
+
if (validationFailure)
|
|
609
|
+
return { result: validationFailure };
|
|
610
|
+
if (labelsAreReview) {
|
|
611
|
+
try {
|
|
612
|
+
await this.dependencies.issues.setLabels(active.record.issueNumber, runningLabels);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return { result: await this.invokedFailure(active, 'review-feedback-activation-labels-delivery-unknown') };
|
|
616
|
+
}
|
|
617
|
+
const observed = await this.readIssue(active.record.issueNumber);
|
|
618
|
+
if (!observed || !sameStrings(observed.labels, runningLabels)
|
|
619
|
+
|| !this.hasTrustedClaim(observed, active.record.runId, active.record.issueNumber, active.record.branchName)) {
|
|
620
|
+
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-labels-diverged') };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (intent)
|
|
624
|
+
active = await this.confirmEffect(active);
|
|
625
|
+
let worktree = await this.dependencies.git.inspectWorktree({
|
|
626
|
+
worktreePath: active.record.worktreePath,
|
|
627
|
+
branchName: active.record.branchName,
|
|
628
|
+
baseSha: expectedHeadSha,
|
|
629
|
+
});
|
|
630
|
+
if (worktree === 'absent' && this.dependencies.git.ensureContinuationWorktree) {
|
|
631
|
+
try {
|
|
632
|
+
await this.dependencies.git.ensureContinuationWorktree({
|
|
633
|
+
targetRoot,
|
|
634
|
+
worktreePath: active.record.worktreePath,
|
|
635
|
+
branchName: active.record.branchName,
|
|
636
|
+
baseBranch: config.github.baseBranch,
|
|
637
|
+
publishedHeadSha: expectedHeadSha,
|
|
638
|
+
});
|
|
639
|
+
worktree = await this.dependencies.git.inspectWorktree({
|
|
640
|
+
worktreePath: active.record.worktreePath,
|
|
641
|
+
branchName: active.record.branchName,
|
|
642
|
+
baseSha: expectedHeadSha,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
worktree = 'diverged';
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const remoteSha = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
|
|
650
|
+
if (worktree !== 'matching' || remoteSha !== expectedHeadSha
|
|
651
|
+
|| (await this.dependencies.git.listChangedFilesIgnoringUntrackedRoot(active.record.worktreePath, config.proof.artifactDir)).length !== 0) {
|
|
652
|
+
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-worktree-diverged') };
|
|
653
|
+
}
|
|
654
|
+
active = await this.persist(active, { reviewFeedback: markReviewFeedbackRepairing(active.record.reviewFeedback) });
|
|
655
|
+
return { active };
|
|
656
|
+
}
|
|
657
|
+
async reviewReadyObservationBlocked(active, code) {
|
|
658
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary: 'Review feedback observation failed closed before activation.' });
|
|
659
|
+
const terminalOutcome = {
|
|
660
|
+
status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path,
|
|
661
|
+
};
|
|
662
|
+
await this.persist(active, {
|
|
663
|
+
lifecycle: 'blocked',
|
|
664
|
+
reviewFeedback: active.record.reviewFeedback,
|
|
665
|
+
directReview: active.record.directReview && active.record.directReview.status !== 'terminal'
|
|
666
|
+
? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind: 'safety' })
|
|
667
|
+
: active.record.directReview,
|
|
668
|
+
terminalOutcome,
|
|
669
|
+
outcomeEvidenceId: evidence.id,
|
|
670
|
+
intent: undefined,
|
|
671
|
+
});
|
|
672
|
+
return publicOutcome(terminalOutcome);
|
|
673
|
+
}
|
|
267
674
|
async runIssue(input) {
|
|
268
675
|
let owner;
|
|
269
676
|
let active;
|
|
677
|
+
let recoveredFeedbackImplementation;
|
|
270
678
|
try {
|
|
271
679
|
assertPositiveInteger(input.issueNumber, 'issueNumber');
|
|
272
680
|
const targetRoot = resolve(input.targetRoot);
|
|
@@ -335,17 +743,36 @@ export class RunIssue {
|
|
|
335
743
|
if (existing.branchName !== expectedBranch || existing.worktreePath !== expectedWorktree) {
|
|
336
744
|
return await this.preClaimInternal('run-identity-mismatch', input.issueNumber);
|
|
337
745
|
}
|
|
338
|
-
|
|
339
|
-
return publicOutcome(existing.terminalOutcome);
|
|
340
|
-
active = { state: persisted, record: existing };
|
|
746
|
+
active = { state: persisted, record: existing, config };
|
|
341
747
|
issueSnapshot = structuredClone(existing.issueSnapshot);
|
|
342
748
|
frozenCriteria = structuredClone(existing.frozenCriteria);
|
|
343
749
|
runId = existing.runId;
|
|
344
750
|
branchName = existing.branchName;
|
|
345
751
|
worktreePath = existing.worktreePath;
|
|
346
752
|
baseSha = existing.baseSha;
|
|
753
|
+
if (active.record.intent?.kind === 'review-blocked-labels') {
|
|
754
|
+
return await this.blockReviewFeedback(active, active.record.intent.blockKind, active.record.intent.evidenceCode);
|
|
755
|
+
}
|
|
756
|
+
if (existing.terminalOutcome) {
|
|
757
|
+
if (existing.terminalOutcome.status !== 'review-ready')
|
|
758
|
+
return publicOutcome(existing.terminalOutcome);
|
|
759
|
+
const continuation = await this.continueReviewReady(active, targetRoot, config);
|
|
760
|
+
if ('result' in continuation)
|
|
761
|
+
return continuation.result;
|
|
762
|
+
active = continuation.active;
|
|
763
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
764
|
+
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
765
|
+
}
|
|
766
|
+
if (active.record.reviewFeedback?.phase === 'frozen') {
|
|
767
|
+
const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
768
|
+
if ('result' in activation)
|
|
769
|
+
return activation.result;
|
|
770
|
+
active = activation.active;
|
|
771
|
+
}
|
|
347
772
|
if (existing.lifecycle === 'publishing') {
|
|
348
|
-
return
|
|
773
|
+
return active.record.reviewFeedback?.activeBatch
|
|
774
|
+
? await this.updateExistingPullRequest(active, config, input.issueNumber)
|
|
775
|
+
: await this.publish(active, config, issueSnapshot, input.issueNumber);
|
|
349
776
|
}
|
|
350
777
|
if (existing.lifecycle === 'claimed') {
|
|
351
778
|
const claim = await this.reconcileClaim(active, config);
|
|
@@ -392,6 +819,37 @@ export class RunIssue {
|
|
|
392
819
|
return await this.revoked(active);
|
|
393
820
|
return await this.continueSpecRequired(active);
|
|
394
821
|
}
|
|
822
|
+
const feedbackInvocation = active.record.reviewFeedback?.implementationInvocation;
|
|
823
|
+
if (feedbackInvocation?.phase === 'prepared') {
|
|
824
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-launch-ambiguous');
|
|
825
|
+
}
|
|
826
|
+
if (feedbackInvocation?.phase === 'launched') {
|
|
827
|
+
try {
|
|
828
|
+
await this.dependencies.waitForReviewProcessAbsence(feedbackInvocation.processGroupId);
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-process-absence-unconfirmed');
|
|
832
|
+
}
|
|
833
|
+
if (!this.dependencies.implementationAgent.recover) {
|
|
834
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-unavailable');
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
recoveredFeedbackImplementation = await this.dependencies.implementationAgent.recover({
|
|
838
|
+
runId: active.record.runId,
|
|
839
|
+
canonicalRepository: active.record.canonicalRepository,
|
|
840
|
+
invocation: feedbackInvocation,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-failed');
|
|
845
|
+
}
|
|
846
|
+
if (!recoveredFeedbackImplementation) {
|
|
847
|
+
const snapshot = await this.dependencies.git.snapshot(active.record.worktreePath);
|
|
848
|
+
if (!sameFreshness(snapshot, feedbackInvocation.baseline)) {
|
|
849
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
395
853
|
if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
396
854
|
return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
|
|
397
855
|
}
|
|
@@ -404,8 +862,12 @@ export class RunIssue {
|
|
|
404
862
|
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
|
|
405
863
|
const checkRecovery = active.record.lifecycle === 'checking'
|
|
406
864
|
&& active.record.directReview?.status === 'clear';
|
|
407
|
-
|
|
408
|
-
|
|
865
|
+
const proofRecovery = active.record.lifecycle === 'proving'
|
|
866
|
+
&& active.record.directReview?.status === 'clear';
|
|
867
|
+
const feedbackRepair = active.record.reviewFeedback?.phase === 'repairing'
|
|
868
|
+
&& active.record.directReview?.stage === 'review-repair';
|
|
869
|
+
if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair) {
|
|
870
|
+
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
409
871
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
410
872
|
}
|
|
411
873
|
active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
|
|
@@ -430,7 +892,7 @@ export class RunIssue {
|
|
|
430
892
|
const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
|
|
431
893
|
active = await this.createRun({
|
|
432
894
|
runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
|
|
433
|
-
issueSnapshot, frozenCriteria,
|
|
895
|
+
issueSnapshot, frozenCriteria, config,
|
|
434
896
|
intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
|
|
435
897
|
});
|
|
436
898
|
const claim = await this.reconcileClaim(active, config);
|
|
@@ -452,7 +914,7 @@ export class RunIssue {
|
|
|
452
914
|
return routed.result;
|
|
453
915
|
active = routed.active;
|
|
454
916
|
}
|
|
455
|
-
if (
|
|
917
|
+
if (!['implementing', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
456
918
|
return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
|
|
457
919
|
}
|
|
458
920
|
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
@@ -464,7 +926,7 @@ export class RunIssue {
|
|
|
464
926
|
}
|
|
465
927
|
if (this.signal.aborted)
|
|
466
928
|
return await this.terminal(active, { status: 'cancelled' });
|
|
467
|
-
let resumeAtChecks = active.record.lifecycle === 'checking'
|
|
929
|
+
let resumeAtChecks = (active.record.lifecycle === 'checking' || active.record.lifecycle === 'proving')
|
|
468
930
|
&& active.record.directReview?.status === 'clear';
|
|
469
931
|
if (active.record.lifecycle === 'implementing'
|
|
470
932
|
&& active.record.directReview?.status === 'active'
|
|
@@ -473,7 +935,7 @@ export class RunIssue {
|
|
|
473
935
|
if ('status' in recovered)
|
|
474
936
|
return recovered;
|
|
475
937
|
active = recovered;
|
|
476
|
-
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
938
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
|
|
477
939
|
if ('status' in reviewed)
|
|
478
940
|
return reviewed;
|
|
479
941
|
active = reviewed;
|
|
@@ -482,8 +944,40 @@ export class RunIssue {
|
|
|
482
944
|
resumeAtChecks = true;
|
|
483
945
|
}
|
|
484
946
|
if (!resumeAtChecks) {
|
|
947
|
+
const feedbackBatch = active.record.reviewFeedback?.activeBatch;
|
|
948
|
+
const workerBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
949
|
+
if (workerBlock)
|
|
950
|
+
return workerBlock;
|
|
951
|
+
const feedbackProjection = feedbackBatch
|
|
952
|
+
? projectReviewFeedbackBatch(feedbackBatch, active.record.directReview.targetRevision)
|
|
953
|
+
: undefined;
|
|
954
|
+
const feedbackLaunch = feedbackBatch ? {
|
|
955
|
+
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
956
|
+
reviewFeedback: feedbackProjection.workerFeedback,
|
|
957
|
+
onPrepared: async (invocation) => {
|
|
958
|
+
const currentActive = active;
|
|
959
|
+
active = await this.persist(currentActive, {
|
|
960
|
+
reviewFeedback: {
|
|
961
|
+
...structuredClone(currentActive.record.reviewFeedback),
|
|
962
|
+
implementationInvocation: { ...structuredClone(invocation), phase: 'prepared', pid: null, processGroupId: null, launchedAt: null },
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
},
|
|
966
|
+
onLaunched: async (invocation) => {
|
|
967
|
+
const currentActive = active;
|
|
968
|
+
const current = currentActive.record.reviewFeedback?.implementationInvocation;
|
|
969
|
+
if (!current || current.phase !== 'prepared' || current.attemptId !== invocation.attemptId)
|
|
970
|
+
throw new Error('review feedback implementation launch mismatch');
|
|
971
|
+
active = await this.persist(currentActive, {
|
|
972
|
+
reviewFeedback: {
|
|
973
|
+
...structuredClone(currentActive.record.reviewFeedback),
|
|
974
|
+
implementationInvocation: { ...current, phase: 'launched', pid: invocation.pid, processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt },
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
},
|
|
978
|
+
} : {};
|
|
485
979
|
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
486
|
-
let implementation = await this.runImplementation({
|
|
980
|
+
let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
|
|
487
981
|
runId,
|
|
488
982
|
worktreePath,
|
|
489
983
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -492,7 +986,9 @@ export class RunIssue {
|
|
|
492
986
|
reworkFindings: active.record.reworkFindings,
|
|
493
987
|
repairOnly: false,
|
|
494
988
|
workflowGeneration: active.record.workflowGeneration,
|
|
989
|
+
...feedbackLaunch,
|
|
495
990
|
});
|
|
991
|
+
recoveredFeedbackImplementation = undefined;
|
|
496
992
|
if (implementation.kind === 'safe-halt') {
|
|
497
993
|
active = await this.persist(active, {
|
|
498
994
|
lifecycle: 'safe-halt',
|
|
@@ -521,6 +1017,10 @@ export class RunIssue {
|
|
|
521
1017
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
522
1018
|
}
|
|
523
1019
|
if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
|
|
1020
|
+
const invocation = active.record.reviewFeedback?.implementationInvocation;
|
|
1021
|
+
if (invocation && !sameFreshness(await this.dependencies.git.snapshot(worktreePath), invocation.baseline)) {
|
|
1022
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
|
|
1023
|
+
}
|
|
524
1024
|
active = await this.persist(active, { transportRetries: 1 });
|
|
525
1025
|
continue attemptLoop;
|
|
526
1026
|
}
|
|
@@ -536,6 +1036,9 @@ export class RunIssue {
|
|
|
536
1036
|
}
|
|
537
1037
|
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
538
1038
|
active = await this.persist(active, { reportRepairs: 1 });
|
|
1039
|
+
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1040
|
+
if (repairBlock)
|
|
1041
|
+
return repairBlock;
|
|
539
1042
|
implementation = await this.runImplementation({
|
|
540
1043
|
runId,
|
|
541
1044
|
worktreePath,
|
|
@@ -545,6 +1048,7 @@ export class RunIssue {
|
|
|
545
1048
|
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
546
1049
|
repairOnly: true,
|
|
547
1050
|
workflowGeneration: active.record.workflowGeneration,
|
|
1051
|
+
...feedbackLaunch,
|
|
548
1052
|
});
|
|
549
1053
|
if (implementation.kind === 'safe-halt') {
|
|
550
1054
|
active = await this.persist(active, {
|
|
@@ -583,7 +1087,7 @@ export class RunIssue {
|
|
|
583
1087
|
if (report.status === 'external-block') {
|
|
584
1088
|
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
585
1089
|
}
|
|
586
|
-
if (await this.dependencies.git.getHead(worktreePath) !==
|
|
1090
|
+
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
587
1091
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
588
1092
|
}
|
|
589
1093
|
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
@@ -593,6 +1097,9 @@ export class RunIssue {
|
|
|
593
1097
|
}
|
|
594
1098
|
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
595
1099
|
active = await this.persist(active, { reportRepairs: 1 });
|
|
1100
|
+
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1101
|
+
if (repairBlock)
|
|
1102
|
+
return repairBlock;
|
|
596
1103
|
implementation = await this.runImplementation({
|
|
597
1104
|
runId,
|
|
598
1105
|
worktreePath,
|
|
@@ -602,6 +1109,7 @@ export class RunIssue {
|
|
|
602
1109
|
reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
|
|
603
1110
|
repairOnly: true,
|
|
604
1111
|
workflowGeneration: active.record.workflowGeneration,
|
|
1112
|
+
...feedbackLaunch,
|
|
605
1113
|
});
|
|
606
1114
|
if (implementation.kind === 'safe-halt') {
|
|
607
1115
|
active = await this.persist(active, {
|
|
@@ -655,6 +1163,9 @@ export class RunIssue {
|
|
|
655
1163
|
if (active.record.directReview?.stage === 'review-repair') {
|
|
656
1164
|
active = await this.persist(active, {
|
|
657
1165
|
directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
|
|
1166
|
+
...(active.record.reviewFeedback ? {
|
|
1167
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1168
|
+
} : {}),
|
|
658
1169
|
});
|
|
659
1170
|
}
|
|
660
1171
|
else {
|
|
@@ -665,9 +1176,12 @@ export class RunIssue {
|
|
|
665
1176
|
targetFingerprint,
|
|
666
1177
|
codeReviewerSessionId: reviewerSessionId,
|
|
667
1178
|
}),
|
|
1179
|
+
...(active.record.reviewFeedback ? {
|
|
1180
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1181
|
+
} : {}),
|
|
668
1182
|
});
|
|
669
1183
|
}
|
|
670
|
-
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
1184
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
|
|
671
1185
|
if ('status' in reviewed)
|
|
672
1186
|
return reviewed;
|
|
673
1187
|
active = reviewed;
|
|
@@ -675,7 +1189,12 @@ export class RunIssue {
|
|
|
675
1189
|
continue attemptLoop;
|
|
676
1190
|
}
|
|
677
1191
|
else {
|
|
678
|
-
active = await this.persist(active, {
|
|
1192
|
+
active = await this.persist(active, {
|
|
1193
|
+
lifecycle: 'checking',
|
|
1194
|
+
...(active.record.reviewFeedback ? {
|
|
1195
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1196
|
+
} : {}),
|
|
1197
|
+
});
|
|
679
1198
|
}
|
|
680
1199
|
}
|
|
681
1200
|
for (const [id, command] of Object.entries(config.checks)) {
|
|
@@ -695,8 +1214,10 @@ export class RunIssue {
|
|
|
695
1214
|
const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
|
|
696
1215
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
697
1216
|
if (check.status !== 'passed') {
|
|
698
|
-
if (active
|
|
699
|
-
return
|
|
1217
|
+
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1218
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1219
|
+
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
1220
|
+
: await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
700
1221
|
}
|
|
701
1222
|
const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
|
|
702
1223
|
active = await this.startNextCycle(active, [summary], [{
|
|
@@ -734,7 +1255,7 @@ export class RunIssue {
|
|
|
734
1255
|
catch {
|
|
735
1256
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
736
1257
|
}
|
|
737
|
-
if (await this.dependencies.git.getHead(worktreePath) !==
|
|
1258
|
+
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
738
1259
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
739
1260
|
}
|
|
740
1261
|
const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
@@ -750,7 +1271,7 @@ export class RunIssue {
|
|
|
750
1271
|
runId,
|
|
751
1272
|
issueNumber: input.issueNumber,
|
|
752
1273
|
cycle: active.record.cycle,
|
|
753
|
-
baseSha,
|
|
1274
|
+
baseSha: expectedImplementationHead(active.record),
|
|
754
1275
|
headSha: freshness.headSha,
|
|
755
1276
|
indexTreeSha: freshness.indexTreeSha,
|
|
756
1277
|
trackedContentSha256: freshness.trackedContentSha256,
|
|
@@ -775,9 +1296,16 @@ export class RunIssue {
|
|
|
775
1296
|
frozenCriteria,
|
|
776
1297
|
checkedChange,
|
|
777
1298
|
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1299
|
+
beforeAgentLaunch: async () => {
|
|
1300
|
+
const failure = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1301
|
+
if (failure)
|
|
1302
|
+
throw new ProofLaunchAuthorizationError(failure);
|
|
1303
|
+
},
|
|
778
1304
|
});
|
|
779
1305
|
}
|
|
780
1306
|
catch (error) {
|
|
1307
|
+
if (error instanceof ProofLaunchAuthorizationError)
|
|
1308
|
+
return error.outcome;
|
|
781
1309
|
if (error instanceof ProofQuiescenceError) {
|
|
782
1310
|
active = await this.persist(active, {
|
|
783
1311
|
lifecycle: 'safe-halt',
|
|
@@ -813,8 +1341,10 @@ export class RunIssue {
|
|
|
813
1341
|
if (this.signal.aborted)
|
|
814
1342
|
return await this.terminal(active, { status: 'cancelled' });
|
|
815
1343
|
if (proof.status === 'needs-rework') {
|
|
816
|
-
if (active
|
|
817
|
-
return
|
|
1344
|
+
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1345
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1346
|
+
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-proof-repair-exhausted')
|
|
1347
|
+
: await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
818
1348
|
}
|
|
819
1349
|
active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
|
|
820
1350
|
provenance: 'proof',
|
|
@@ -826,10 +1356,23 @@ export class RunIssue {
|
|
|
826
1356
|
}
|
|
827
1357
|
if (proof.status !== 'passed')
|
|
828
1358
|
return await this.mapProofFailure(active, proof);
|
|
829
|
-
active = await this.persist(active, {
|
|
1359
|
+
active = await this.persist(active, {
|
|
1360
|
+
lifecycle: 'publishing',
|
|
1361
|
+
proofReceipt: proof.receipt,
|
|
1362
|
+
reworkFindings: [],
|
|
1363
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
1364
|
+
reviewFeedback: markReviewFeedbackPublishing(markReviewFeedbackVerified(active.record.reviewFeedback, {
|
|
1365
|
+
checkedChangeSha256,
|
|
1366
|
+
proofId,
|
|
1367
|
+
verifiedAt: this.timestamp(),
|
|
1368
|
+
})),
|
|
1369
|
+
} : {}),
|
|
1370
|
+
});
|
|
830
1371
|
break;
|
|
831
1372
|
}
|
|
832
|
-
return
|
|
1373
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1374
|
+
? await this.updateExistingPullRequest(active, config, input.issueNumber)
|
|
1375
|
+
: await this.publish(active, config, issueSnapshot, input.issueNumber);
|
|
833
1376
|
}
|
|
834
1377
|
catch (error) {
|
|
835
1378
|
if (!active && error instanceof TransportReadError) {
|
|
@@ -1223,8 +1766,9 @@ export class RunIssue {
|
|
|
1223
1766
|
const state = await this.dependencies.runRecords.read();
|
|
1224
1767
|
const now = this.timestamp();
|
|
1225
1768
|
const workflow = await this.dependencies.createWorkflowGeneration();
|
|
1769
|
+
const { config, ...persistedInput } = input;
|
|
1226
1770
|
const record = {
|
|
1227
|
-
...
|
|
1771
|
+
...persistedInput,
|
|
1228
1772
|
lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
|
|
1229
1773
|
reworkFindings: [],
|
|
1230
1774
|
packageVersion: workflow.receipt.packageVersion,
|
|
@@ -1233,9 +1777,9 @@ export class RunIssue {
|
|
|
1233
1777
|
checks: [], createdAt: now, updatedAt: now,
|
|
1234
1778
|
};
|
|
1235
1779
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1236
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
1780
|
+
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
1237
1781
|
});
|
|
1238
|
-
return { state: saved, record: findRun(saved, input.runId) };
|
|
1782
|
+
return { state: saved, record: findRun(saved, input.runId), config };
|
|
1239
1783
|
}
|
|
1240
1784
|
async persist(active, changes) {
|
|
1241
1785
|
const record = { ...active.record, ...changes, updatedAt: this.timestamp() };
|
|
@@ -1243,15 +1787,15 @@ export class RunIssue {
|
|
|
1243
1787
|
delete record.intent;
|
|
1244
1788
|
if (Object.hasOwn(changes, 'process') && changes.process === undefined)
|
|
1245
1789
|
delete record.process;
|
|
1246
|
-
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
|
|
1790
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback']) {
|
|
1247
1791
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1248
1792
|
delete record[key];
|
|
1249
1793
|
}
|
|
1250
1794
|
const runs = active.state.runs.map((candidate) => candidate.runId === record.runId ? record : candidate);
|
|
1251
1795
|
const saved = await this.dependencies.runRecords.compareAndSwap(active.state.generation, {
|
|
1252
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
1796
|
+
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs,
|
|
1253
1797
|
});
|
|
1254
|
-
return { state: saved, record: findRun(saved, record.runId) };
|
|
1798
|
+
return { state: saved, record: findRun(saved, record.runId), config: active.config };
|
|
1255
1799
|
}
|
|
1256
1800
|
clearIntent(active) {
|
|
1257
1801
|
return this.persist(active, { intent: undefined });
|
|
@@ -1271,6 +1815,14 @@ export class RunIssue {
|
|
|
1271
1815
|
const labels = new Set(issue.labels);
|
|
1272
1816
|
if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
|
|
1273
1817
|
return false;
|
|
1818
|
+
return this.hasTrustedClaim(issue, runId, issueNumber, branchName);
|
|
1819
|
+
}
|
|
1820
|
+
async authorizedForExactLabels(issueNumber, runId, branchName, expectedLabels) {
|
|
1821
|
+
const issue = await this.readIssue(issueNumber);
|
|
1822
|
+
return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
|
|
1823
|
+
&& this.hasTrustedClaim(issue, runId, issueNumber, branchName);
|
|
1824
|
+
}
|
|
1825
|
+
hasTrustedClaim(issue, runId, issueNumber, branchName) {
|
|
1274
1826
|
const exactBody = claimComment(runId, issueNumber, branchName);
|
|
1275
1827
|
const markerPattern = /^<!-- codex-orchestrator:run:([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):claim -->$/u;
|
|
1276
1828
|
const markers = issue.comments.filter((comment) => markerPattern.test(comment.body.split('\n')[0] ?? ''));
|
|
@@ -1292,6 +1844,30 @@ export class RunIssue {
|
|
|
1292
1844
|
throw new TransportReadError();
|
|
1293
1845
|
}
|
|
1294
1846
|
}
|
|
1847
|
+
async revalidateFeedbackWorker(active, config, issueNumber) {
|
|
1848
|
+
const batch = active.record.reviewFeedback?.activeBatch;
|
|
1849
|
+
if (!batch)
|
|
1850
|
+
return undefined;
|
|
1851
|
+
if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
|
|
1852
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-authority-revoked');
|
|
1853
|
+
}
|
|
1854
|
+
if (!this.dependencies.reviewFeedback) {
|
|
1855
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-revalidation-unavailable');
|
|
1856
|
+
}
|
|
1857
|
+
const validation = await this.dependencies.reviewFeedback.revalidate({
|
|
1858
|
+
batch,
|
|
1859
|
+
epoch: 'pre-update',
|
|
1860
|
+
expectedHeadSha: batch.priorPublishedHeadSha,
|
|
1861
|
+
});
|
|
1862
|
+
return this.mapFeedbackRevalidation(active, validation, 'review-feedback-worker-revalidation-failed');
|
|
1863
|
+
}
|
|
1864
|
+
async mapFeedbackRevalidation(active, validation, safetyCode) {
|
|
1865
|
+
if (validation.status === 'valid')
|
|
1866
|
+
return undefined;
|
|
1867
|
+
if (validation.status === 'retryable')
|
|
1868
|
+
return this.invokedFailure(active, `${safetyCode}-retryable`);
|
|
1869
|
+
return this.blockReviewFeedback(active, 'safety', safetyCode);
|
|
1870
|
+
}
|
|
1295
1871
|
async runImplementation(input) {
|
|
1296
1872
|
try {
|
|
1297
1873
|
return await this.dependencies.implementationAgent.run({
|
|
@@ -1304,7 +1880,7 @@ export class RunIssue {
|
|
|
1304
1880
|
return { kind: 'internal-error' };
|
|
1305
1881
|
}
|
|
1306
1882
|
}
|
|
1307
|
-
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
|
|
1883
|
+
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles, config) {
|
|
1308
1884
|
let active = starting;
|
|
1309
1885
|
let reportRepair;
|
|
1310
1886
|
while (true) {
|
|
@@ -1321,6 +1897,9 @@ export class RunIssue {
|
|
|
1321
1897
|
? directReviewClosureRequestSha256(directReview)
|
|
1322
1898
|
: null;
|
|
1323
1899
|
const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
|
|
1900
|
+
const reviewerAuthorizationFailure = await this.revalidateFeedbackWorker(active, config, active.record.issueNumber);
|
|
1901
|
+
if (reviewerAuthorizationFailure)
|
|
1902
|
+
return reviewerAuthorizationFailure;
|
|
1324
1903
|
const result = await this.dependencies.implementationReviewer.run({
|
|
1325
1904
|
runId: active.record.runId,
|
|
1326
1905
|
worktreePath: active.record.worktreePath,
|
|
@@ -1369,8 +1948,10 @@ export class RunIssue {
|
|
|
1369
1948
|
if (!current)
|
|
1370
1949
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1371
1950
|
if (result.report.verdict === 'needs-work') {
|
|
1372
|
-
if (active
|
|
1373
|
-
return
|
|
1951
|
+
if (this.repairBudgetExhausted(active, maxCycles)) {
|
|
1952
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1953
|
+
? this.blockReviewFeedback(active, 'exhausted', 'direct-review-repair-exhausted')
|
|
1954
|
+
: this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
|
|
1374
1955
|
}
|
|
1375
1956
|
const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
|
|
1376
1957
|
const findings = [
|
|
@@ -1383,7 +1964,12 @@ export class RunIssue {
|
|
|
1383
1964
|
];
|
|
1384
1965
|
return this.persist(active, {
|
|
1385
1966
|
lifecycle: 'implementing',
|
|
1386
|
-
cycle:
|
|
1967
|
+
cycle: active.record.reviewFeedback?.activeBatch
|
|
1968
|
+
? active.record.cycle
|
|
1969
|
+
: (active.record.cycle + 1),
|
|
1970
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
1971
|
+
reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
|
|
1972
|
+
} : {}),
|
|
1387
1973
|
directReview: repaired,
|
|
1388
1974
|
reworkFindings: findings,
|
|
1389
1975
|
checks: [],
|
|
@@ -1505,7 +2091,12 @@ export class RunIssue {
|
|
|
1505
2091
|
}));
|
|
1506
2092
|
return this.persist(active, {
|
|
1507
2093
|
lifecycle: 'implementing',
|
|
1508
|
-
cycle:
|
|
2094
|
+
cycle: active.record.reviewFeedback?.activeBatch
|
|
2095
|
+
? active.record.cycle
|
|
2096
|
+
: (active.record.cycle + 1),
|
|
2097
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2098
|
+
reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
|
|
2099
|
+
} : {}),
|
|
1509
2100
|
reworkFindings: [...findings],
|
|
1510
2101
|
directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
|
|
1511
2102
|
checks: [],
|
|
@@ -1517,13 +2108,81 @@ export class RunIssue {
|
|
|
1517
2108
|
active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
|
|
1518
2109
|
return this.persist(active, {
|
|
1519
2110
|
lifecycle: 'implementing',
|
|
1520
|
-
cycle:
|
|
2111
|
+
cycle: active.record.reviewFeedback?.activeBatch
|
|
2112
|
+
? active.record.cycle
|
|
2113
|
+
: (active.record.cycle + 1),
|
|
2114
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2115
|
+
reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
|
|
2116
|
+
} : {}),
|
|
1521
2117
|
checks: [],
|
|
1522
2118
|
checkedChangeSha256: undefined,
|
|
1523
2119
|
proofId: undefined,
|
|
1524
2120
|
proofReceipt: undefined,
|
|
1525
2121
|
});
|
|
1526
2122
|
}
|
|
2123
|
+
repairBudgetExhausted(active, maxCycles) {
|
|
2124
|
+
return active.record.reviewFeedback?.activeBatch
|
|
2125
|
+
? active.record.reviewFeedback.repairRound >= 3
|
|
2126
|
+
: active.record.cycle >= maxCycles;
|
|
2127
|
+
}
|
|
2128
|
+
async blockReviewFeedback(active, kind, evidenceCode) {
|
|
2129
|
+
const feedback = active.record.reviewFeedback;
|
|
2130
|
+
if (!feedback?.activeBatch)
|
|
2131
|
+
return this.terminal(active, { status: 'blocked', kind, resumable: false }, evidenceCode);
|
|
2132
|
+
{
|
|
2133
|
+
const config = active.config;
|
|
2134
|
+
const blockedLabels = [config.github.labels.auto.name, config.github.labels.blocked.name].sort();
|
|
2135
|
+
const existingIntent = active.record.intent;
|
|
2136
|
+
if (existingIntent?.kind !== 'review-blocked-labels') {
|
|
2137
|
+
active = await this.persist(active, { intent: {
|
|
2138
|
+
kind: 'review-blocked-labels', issueNumber: active.record.issueNumber,
|
|
2139
|
+
batchId: feedback.activeBatch.batchId, expected: blockedLabels, blockKind: kind, evidenceCode,
|
|
2140
|
+
} });
|
|
2141
|
+
}
|
|
2142
|
+
else if (existingIntent.issueNumber !== active.record.issueNumber
|
|
2143
|
+
|| existingIntent.batchId !== feedback.activeBatch.batchId
|
|
2144
|
+
|| !sameStrings(existingIntent.expected, blockedLabels)
|
|
2145
|
+
|| existingIntent.blockKind !== kind
|
|
2146
|
+
|| existingIntent.evidenceCode !== evidenceCode) {
|
|
2147
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-intent-diverged');
|
|
2148
|
+
}
|
|
2149
|
+
let issue = await this.readIssue(active.record.issueNumber);
|
|
2150
|
+
const ownedRunningLabels = [config.github.labels.auto.name, config.github.labels.running.name].sort();
|
|
2151
|
+
const ownedReviewLabels = [config.github.labels.review.name];
|
|
2152
|
+
const canReduceAuthority = !!issue && issue.state === 'OPEN'
|
|
2153
|
+
&& (sameStrings(issue.labels, ownedRunningLabels) || sameStrings(issue.labels, ownedReviewLabels));
|
|
2154
|
+
if (issue?.state === 'OPEN' && !sameStrings(issue.labels, blockedLabels) && !canReduceAuthority) {
|
|
2155
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-source-diverged');
|
|
2156
|
+
}
|
|
2157
|
+
if (canReduceAuthority && !sameStrings(issue.labels, blockedLabels)) {
|
|
2158
|
+
try {
|
|
2159
|
+
await this.dependencies.issues.setLabels(active.record.issueNumber, blockedLabels);
|
|
2160
|
+
}
|
|
2161
|
+
catch {
|
|
2162
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-delivery-unknown');
|
|
2163
|
+
}
|
|
2164
|
+
issue = await this.readIssue(active.record.issueNumber);
|
|
2165
|
+
}
|
|
2166
|
+
if (canReduceAuthority && (!issue || !sameStrings(issue.labels, blockedLabels))) {
|
|
2167
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-observation-diverged');
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: `review feedback ${kind}` });
|
|
2171
|
+
const terminalOutcome = { status: 'blocked', kind, resumable: false, evidencePath: evidence.path };
|
|
2172
|
+
const directReview = active.record.directReview && active.record.directReview.status !== 'terminal'
|
|
2173
|
+
? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind })
|
|
2174
|
+
: active.record.directReview;
|
|
2175
|
+
await this.persist(active, {
|
|
2176
|
+
lifecycle: 'blocked',
|
|
2177
|
+
reviewFeedback: blockReviewFeedback(feedback, kind, this.timestamp()),
|
|
2178
|
+
...(directReview ? { directReview } : {}),
|
|
2179
|
+
terminalOutcome,
|
|
2180
|
+
outcomeEvidenceId: evidence.id,
|
|
2181
|
+
process: undefined,
|
|
2182
|
+
intent: undefined,
|
|
2183
|
+
});
|
|
2184
|
+
return publicOutcome(terminalOutcome);
|
|
2185
|
+
}
|
|
1527
2186
|
async mapImplementationFailure(active, result) {
|
|
1528
2187
|
if (result.kind === 'transport-failed')
|
|
1529
2188
|
return this.terminal(active, { status: 'transport-failed', resumable: result.resumable });
|
|
@@ -1550,6 +2209,9 @@ export class RunIssue {
|
|
|
1550
2209
|
return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
|
|
1551
2210
|
}
|
|
1552
2211
|
async terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
|
|
2212
|
+
if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
|
|
2213
|
+
return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
|
|
2214
|
+
}
|
|
1553
2215
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
|
|
1554
2216
|
const terminalOutcome = { ...outcome, evidencePath: evidence.path };
|
|
1555
2217
|
const changes = {
|
|
@@ -1751,6 +2413,9 @@ function sameFreshness(left, right) {
|
|
|
1751
2413
|
&& left.untrackedContentSha256 === right.untrackedContentSha256
|
|
1752
2414
|
&& left.worktreeIdentity === right.worktreeIdentity;
|
|
1753
2415
|
}
|
|
2416
|
+
function expectedImplementationHead(record) {
|
|
2417
|
+
return record.reviewFeedback?.activeBatch?.priorPublishedHeadSha ?? record.baseSha;
|
|
2418
|
+
}
|
|
1754
2419
|
function commentsWithMarker(issue, marker) {
|
|
1755
2420
|
return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
1756
2421
|
}
|