codex-orchestrator 2.0.3 → 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 +51 -427
- package/README.md +161 -37
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- 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-issue-adapter.d.ts.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.js +6 -7
- package/dist/src/v2/adapters/gh-issue-adapter.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-contract.d.ts +3 -3
- package/dist/src/v2/cli-contract.d.ts.map +1 -1
- package/dist/src/v2/cli-contract.js +1 -3
- package/dist/src/v2/cli-contract.js.map +1 -1
- package/dist/src/v2/cli.d.ts +33 -0
- package/dist/src/v2/cli.d.ts.map +1 -0
- package/dist/src/v2/{candidate-cli.js → cli.js} +55 -39
- package/dist/src/v2/cli.js.map +1 -0
- package/dist/src/v2/code-review-report.d.ts +1 -1
- package/dist/src/v2/code-review-report.d.ts.map +1 -1
- package/dist/src/v2/code-review-report.js +2 -2
- package/dist/src/v2/code-review-report.js.map +1 -1
- package/dist/src/v2/codex-process.d.ts.map +1 -1
- package/dist/src/v2/codex-process.js +12 -1
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/config.d.ts +2 -3
- 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/contained-report-operation.d.ts +2 -2
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
- package/dist/src/v2/contained-report-operation.js +1 -1
- package/dist/src/v2/contained-report-operation.js.map +1 -1
- package/dist/src/v2/containment.d.ts +15 -2
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +43 -6
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +5 -10
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +32 -91
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +55 -29
- package/dist/src/v2/proof-report.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 +63 -9
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +798 -78
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +49 -5
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +138 -44
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +40 -3
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +245 -52
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/setup-cli.d.ts.map +1 -1
- package/dist/src/v2/setup-cli.js +4 -11
- package/dist/src/v2/setup-cli.js.map +1 -1
- package/dist/src/v2/setup-runtime.d.ts.map +1 -1
- package/dist/src/v2/setup-runtime.js +1 -61
- package/dist/src/v2/setup-runtime.js.map +1 -1
- package/dist/src/v2/setup-store.d.ts +0 -5
- package/dist/src/v2/setup-store.d.ts.map +1 -1
- package/dist/src/v2/setup-store.js +3 -106
- package/dist/src/v2/setup-store.js.map +1 -1
- package/dist/src/v2/setup.d.ts +6 -46
- package/dist/src/v2/setup.d.ts.map +1 -1
- package/dist/src/v2/setup.js +12 -294
- package/dist/src/v2/setup.js.map +1 -1
- package/dist/src/v2/workflow-assets.d.ts +19 -11
- package/dist/src/v2/workflow-assets.d.ts.map +1 -1
- package/dist/src/v2/workflow-assets.js +132 -40
- package/dist/src/v2/workflow-assets.js.map +1 -1
- package/docs/deep-dive.md +328 -56
- package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
- package/internal-workflow/docs/agents/coding-skill-routing.md +116 -196
- package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
- package/internal-workflow/docs/agents/review-gates.md +32 -39
- package/internal-workflow/docs/agents/review-protocol.md +75 -147
- package/internal-workflow/evals/coding-skill-evals.json +84 -0
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/operations/acceptance-proof/SKILL.md +7 -1
- package/internal-workflow/operations/ambiguity-review/SKILL.md +2 -0
- package/internal-workflow/operations/code-review/SKILL.md +21 -1
- package/internal-workflow/operations/implementation/SKILL.md +22 -1
- package/internal-workflow/operations/spec-author/SKILL.md +10 -1
- package/internal-workflow/operations/spec-review/SKILL.md +10 -1
- package/internal-workflow/operations/triage/SKILL.md +10 -1
- package/internal-workflow/schemas/code-review-v1.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/internal-workflow/skills/agent-auto/SKILL.md +6 -1
- package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
- package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
- package/internal-workflow/skills/code-review/SKILL.md +51 -17
- package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +15 -6
- package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
- package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +2 -2
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +108 -204
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
- package/internal-workflow/skills/small-task-implementer/SKILL.md +15 -8
- package/internal-workflow/skills/spec-implementer/SKILL.md +101 -172
- package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
- package/internal-workflow/skills/spec-implementer/references/review-loop.md +100 -0
- package/internal-workflow/skills/tdd/SKILL.md +20 -6
- package/internal-workflow/skills/tdd/agents/openai.yaml +2 -2
- 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 +9 -6
- package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
- package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
- package/dist/src/v2/adapters/target-activity-fence.js +0 -249
- package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
- package/dist/src/v2/candidate-cli.d.ts +0 -26
- package/dist/src/v2/candidate-cli.d.ts.map +0 -1
- package/dist/src/v2/candidate-cli.js.map +0 -1
- package/dist/src/v2/legacy-cutover.d.ts +0 -52
- package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
- package/dist/src/v2/legacy-cutover.js +0 -87
- package/dist/src/v2/legacy-cutover.js.map +0 -1
- package/internal-workflow/docs/agents/artifact-review-loop.md +0 -267
- package/internal-workflow/docs/agents/implementation-review-loop.md +0 -302
- package/internal-workflow/operations/cleanup-review/SKILL.md +0 -3
- package/internal-workflow/operations/spec-implementation/SKILL.md +0 -3
- package/internal-workflow/profiles/implementer_deep.toml +0 -9
- package/internal-workflow/profiles/researcher_standard.toml +0 -9
- package/internal-workflow/profiles/reviewer_fast.toml +0 -9
- package/internal-workflow/skills/cleanup-review/SKILL.md +0 -84
- package/internal-workflow/skills/cleanup-review/agents/openai.yaml +0 -6
- package/internal-workflow/skills/codebase-design/DEEPENING.md +0 -35
- package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +0 -50
- package/internal-workflow/skills/codebase-design/SKILL.md +0 -82
- package/internal-workflow/skills/codebase-design/agents/openai.yaml +0 -6
- package/internal-workflow/skills/research/SKILL.md +0 -107
- package/internal-workflow/skills/research/agents/openai.yaml +0 -6
- package/internal-workflow/skills/ui-evidence-proof/SKILL.md +0 -123
- package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +0 -6
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -3,11 +3,12 @@ import { checkedChangePayloadSha256, } from './checked-change.js';
|
|
|
3
3
|
import { parseAgentAutoConfig } from './config.js';
|
|
4
4
|
import { canonicalJson, sha256 } from './containment.js';
|
|
5
5
|
import { validateImplementationReport } from './implementation-report.js';
|
|
6
|
-
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair,
|
|
7
|
-
import { ProofQuiescenceError } from './acceptance-proof.js';
|
|
8
|
-
import {
|
|
6
|
+
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
|
|
7
|
+
import { ProofLaunchAuthorizationError, ProofQuiescenceError } from './acceptance-proof.js';
|
|
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);
|
|
@@ -363,7 +790,7 @@ export class RunIssue {
|
|
|
363
790
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
364
791
|
}
|
|
365
792
|
}
|
|
366
|
-
active = await this.
|
|
793
|
+
active = await this.initializeClaimedRun(active, issue);
|
|
367
794
|
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
368
795
|
}
|
|
369
796
|
else {
|
|
@@ -377,9 +804,6 @@ export class RunIssue {
|
|
|
377
804
|
catch {
|
|
378
805
|
return await this.invokedFailure(active, 'safe-halt-process-absence-unconfirmed');
|
|
379
806
|
}
|
|
380
|
-
if (!process.purpose || !process.resumeLifecycle || process.resumeReviewStage === undefined) {
|
|
381
|
-
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-migration-unrecoverable' });
|
|
382
|
-
}
|
|
383
807
|
active = await this.persist(active, { lifecycle: process.resumeLifecycle, process: undefined });
|
|
384
808
|
}
|
|
385
809
|
if (existing.lifecycle === 'waiting-human') {
|
|
@@ -395,6 +819,37 @@ export class RunIssue {
|
|
|
395
819
|
return await this.revoked(active);
|
|
396
820
|
return await this.continueSpecRequired(active);
|
|
397
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
|
+
}
|
|
398
853
|
if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
399
854
|
return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
|
|
400
855
|
}
|
|
@@ -402,13 +857,17 @@ export class RunIssue {
|
|
|
402
857
|
return await this.revoked(active);
|
|
403
858
|
if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
|
|
404
859
|
if (!active.record.routeExecution || !active.record.routeReceipt)
|
|
405
|
-
throw new
|
|
860
|
+
throw new RouteInitializationUnrecoverableError();
|
|
406
861
|
const reviewRecovery = active.record.lifecycle === 'implementing' && active.record.directReview?.status === 'active'
|
|
407
862
|
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
|
|
408
863
|
const checkRecovery = active.record.lifecycle === 'checking'
|
|
409
|
-
&&
|
|
410
|
-
|
|
411
|
-
|
|
864
|
+
&& active.record.directReview?.status === 'clear';
|
|
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)) {
|
|
412
871
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
413
872
|
}
|
|
414
873
|
active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
|
|
@@ -433,7 +892,7 @@ export class RunIssue {
|
|
|
433
892
|
const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
|
|
434
893
|
active = await this.createRun({
|
|
435
894
|
runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
|
|
436
|
-
issueSnapshot, frozenCriteria,
|
|
895
|
+
issueSnapshot, frozenCriteria, config,
|
|
437
896
|
intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
|
|
438
897
|
});
|
|
439
898
|
const claim = await this.reconcileClaim(active, config);
|
|
@@ -446,7 +905,7 @@ export class RunIssue {
|
|
|
446
905
|
catch {
|
|
447
906
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
448
907
|
}
|
|
449
|
-
active = await this.
|
|
908
|
+
active = await this.initializeClaimedRun(active, issue);
|
|
450
909
|
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
451
910
|
}
|
|
452
911
|
if (active.record.lifecycle === 'triaging' || active.record.lifecycle === 'routed') {
|
|
@@ -455,20 +914,11 @@ export class RunIssue {
|
|
|
455
914
|
return routed.result;
|
|
456
915
|
active = routed.active;
|
|
457
916
|
}
|
|
458
|
-
if (
|
|
917
|
+
if (!['implementing', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
459
918
|
return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
|
|
460
919
|
}
|
|
461
920
|
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
462
|
-
|
|
463
|
-
const targetFingerprint = directReviewTargetFingerprint({
|
|
464
|
-
snapshot: await this.dependencies.git.snapshot(worktreePath),
|
|
465
|
-
changedFiles,
|
|
466
|
-
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
467
|
-
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
468
|
-
cycle: active.record.cycle,
|
|
469
|
-
frozenCriteria,
|
|
470
|
-
});
|
|
471
|
-
active = await this.persist(active, { directReview: createLegacyBypassDirectReview(targetFingerprint) });
|
|
921
|
+
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
|
|
472
922
|
}
|
|
473
923
|
attemptLoop: while (true) {
|
|
474
924
|
if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
|
|
@@ -476,8 +926,8 @@ export class RunIssue {
|
|
|
476
926
|
}
|
|
477
927
|
if (this.signal.aborted)
|
|
478
928
|
return await this.terminal(active, { status: 'cancelled' });
|
|
479
|
-
let resumeAtChecks = active.record.lifecycle === 'checking'
|
|
480
|
-
&&
|
|
929
|
+
let resumeAtChecks = (active.record.lifecycle === 'checking' || active.record.lifecycle === 'proving')
|
|
930
|
+
&& active.record.directReview?.status === 'clear';
|
|
481
931
|
if (active.record.lifecycle === 'implementing'
|
|
482
932
|
&& active.record.directReview?.status === 'active'
|
|
483
933
|
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure')) {
|
|
@@ -485,7 +935,7 @@ export class RunIssue {
|
|
|
485
935
|
if ('status' in recovered)
|
|
486
936
|
return recovered;
|
|
487
937
|
active = recovered;
|
|
488
|
-
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);
|
|
489
939
|
if ('status' in reviewed)
|
|
490
940
|
return reviewed;
|
|
491
941
|
active = reviewed;
|
|
@@ -494,9 +944,40 @@ export class RunIssue {
|
|
|
494
944
|
resumeAtChecks = true;
|
|
495
945
|
}
|
|
496
946
|
if (!resumeAtChecks) {
|
|
497
|
-
const
|
|
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
|
+
} : {};
|
|
498
979
|
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
499
|
-
let implementation = await this.runImplementation({
|
|
980
|
+
let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
|
|
500
981
|
runId,
|
|
501
982
|
worktreePath,
|
|
502
983
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -505,9 +986,19 @@ export class RunIssue {
|
|
|
505
986
|
reworkFindings: active.record.reworkFindings,
|
|
506
987
|
repairOnly: false,
|
|
507
988
|
workflowGeneration: active.record.workflowGeneration,
|
|
989
|
+
...feedbackLaunch,
|
|
508
990
|
});
|
|
991
|
+
recoveredFeedbackImplementation = undefined;
|
|
509
992
|
if (implementation.kind === 'safe-halt') {
|
|
510
|
-
active = await this.persist(active, {
|
|
993
|
+
active = await this.persist(active, {
|
|
994
|
+
lifecycle: 'safe-halt',
|
|
995
|
+
process: {
|
|
996
|
+
...implementation.process,
|
|
997
|
+
purpose: 'implementation',
|
|
998
|
+
resumeLifecycle: 'implementing',
|
|
999
|
+
resumeReviewStage: null,
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
511
1002
|
while (true) {
|
|
512
1003
|
try {
|
|
513
1004
|
await implementation.waitForAbsence();
|
|
@@ -526,9 +1017,9 @@ export class RunIssue {
|
|
|
526
1017
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
527
1018
|
}
|
|
528
1019
|
if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
|
|
529
|
-
const
|
|
530
|
-
if (!sameFreshness(
|
|
531
|
-
return await this.
|
|
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');
|
|
532
1023
|
}
|
|
533
1024
|
active = await this.persist(active, { transportRetries: 1 });
|
|
534
1025
|
continue attemptLoop;
|
|
@@ -545,6 +1036,9 @@ export class RunIssue {
|
|
|
545
1036
|
}
|
|
546
1037
|
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
547
1038
|
active = await this.persist(active, { reportRepairs: 1 });
|
|
1039
|
+
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1040
|
+
if (repairBlock)
|
|
1041
|
+
return repairBlock;
|
|
548
1042
|
implementation = await this.runImplementation({
|
|
549
1043
|
runId,
|
|
550
1044
|
worktreePath,
|
|
@@ -554,9 +1048,18 @@ export class RunIssue {
|
|
|
554
1048
|
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
555
1049
|
repairOnly: true,
|
|
556
1050
|
workflowGeneration: active.record.workflowGeneration,
|
|
1051
|
+
...feedbackLaunch,
|
|
557
1052
|
});
|
|
558
1053
|
if (implementation.kind === 'safe-halt') {
|
|
559
|
-
active = await this.persist(active, {
|
|
1054
|
+
active = await this.persist(active, {
|
|
1055
|
+
lifecycle: 'safe-halt',
|
|
1056
|
+
process: {
|
|
1057
|
+
...implementation.process,
|
|
1058
|
+
purpose: 'implementation',
|
|
1059
|
+
resumeLifecycle: 'implementing',
|
|
1060
|
+
resumeReviewStage: null,
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
560
1063
|
while (true) {
|
|
561
1064
|
try {
|
|
562
1065
|
await implementation.waitForAbsence();
|
|
@@ -584,12 +1087,69 @@ export class RunIssue {
|
|
|
584
1087
|
if (report.status === 'external-block') {
|
|
585
1088
|
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
586
1089
|
}
|
|
587
|
-
if (await this.dependencies.git.getHead(worktreePath) !==
|
|
1090
|
+
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
588
1091
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
589
1092
|
}
|
|
590
1093
|
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
591
1094
|
if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
|
|
592
|
-
|
|
1095
|
+
if (changedFiles.length === 0 || active.record.reportRepairs >= 1) {
|
|
1096
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
1097
|
+
}
|
|
1098
|
+
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
1099
|
+
active = await this.persist(active, { reportRepairs: 1 });
|
|
1100
|
+
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1101
|
+
if (repairBlock)
|
|
1102
|
+
return repairBlock;
|
|
1103
|
+
implementation = await this.runImplementation({
|
|
1104
|
+
runId,
|
|
1105
|
+
worktreePath,
|
|
1106
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
1107
|
+
frozenCriteria,
|
|
1108
|
+
cycle: active.record.cycle,
|
|
1109
|
+
reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
|
|
1110
|
+
repairOnly: true,
|
|
1111
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
1112
|
+
...feedbackLaunch,
|
|
1113
|
+
});
|
|
1114
|
+
if (implementation.kind === 'safe-halt') {
|
|
1115
|
+
active = await this.persist(active, {
|
|
1116
|
+
lifecycle: 'safe-halt',
|
|
1117
|
+
process: {
|
|
1118
|
+
...implementation.process,
|
|
1119
|
+
purpose: 'implementation',
|
|
1120
|
+
resumeLifecycle: 'implementing',
|
|
1121
|
+
resumeReviewStage: null,
|
|
1122
|
+
},
|
|
1123
|
+
});
|
|
1124
|
+
while (true) {
|
|
1125
|
+
try {
|
|
1126
|
+
await implementation.waitForAbsence();
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
1134
|
+
}
|
|
1135
|
+
if (implementation.kind !== 'completed')
|
|
1136
|
+
return await this.mapImplementationFailure(active, implementation);
|
|
1137
|
+
const afterRepair = await this.dependencies.git.snapshot(worktreePath);
|
|
1138
|
+
if (!sameFreshness(repairBaseline, afterRepair)) {
|
|
1139
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
report = validateImplementationReport(implementation.report);
|
|
1143
|
+
}
|
|
1144
|
+
catch {
|
|
1145
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
1146
|
+
}
|
|
1147
|
+
if (report.status === 'external-block') {
|
|
1148
|
+
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
1149
|
+
}
|
|
1150
|
+
if (!sameStrings(changedFiles, report.changedFiles)) {
|
|
1151
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
1152
|
+
}
|
|
593
1153
|
}
|
|
594
1154
|
if (active.record.routeReceipt?.route === 'direct') {
|
|
595
1155
|
const targetFingerprint = directReviewTargetFingerprint({
|
|
@@ -603,6 +1163,9 @@ export class RunIssue {
|
|
|
603
1163
|
if (active.record.directReview?.stage === 'review-repair') {
|
|
604
1164
|
active = await this.persist(active, {
|
|
605
1165
|
directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
|
|
1166
|
+
...(active.record.reviewFeedback ? {
|
|
1167
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1168
|
+
} : {}),
|
|
606
1169
|
});
|
|
607
1170
|
}
|
|
608
1171
|
else {
|
|
@@ -611,14 +1174,14 @@ export class RunIssue {
|
|
|
611
1174
|
active = await this.persist(active, {
|
|
612
1175
|
directReview: createInitialDirectReview({
|
|
613
1176
|
targetFingerprint,
|
|
614
|
-
cleanupRequired: false,
|
|
615
|
-
cleanupReason: 'bounded final review owns cleanup',
|
|
616
|
-
cleanupReviewerSessionId: null,
|
|
617
1177
|
codeReviewerSessionId: reviewerSessionId,
|
|
618
1178
|
}),
|
|
1179
|
+
...(active.record.reviewFeedback ? {
|
|
1180
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1181
|
+
} : {}),
|
|
619
1182
|
});
|
|
620
1183
|
}
|
|
621
|
-
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);
|
|
622
1185
|
if ('status' in reviewed)
|
|
623
1186
|
return reviewed;
|
|
624
1187
|
active = reviewed;
|
|
@@ -626,7 +1189,12 @@ export class RunIssue {
|
|
|
626
1189
|
continue attemptLoop;
|
|
627
1190
|
}
|
|
628
1191
|
else {
|
|
629
|
-
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
|
+
});
|
|
630
1198
|
}
|
|
631
1199
|
}
|
|
632
1200
|
for (const [id, command] of Object.entries(config.checks)) {
|
|
@@ -646,8 +1214,10 @@ export class RunIssue {
|
|
|
646
1214
|
const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
|
|
647
1215
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
648
1216
|
if (check.status !== 'passed') {
|
|
649
|
-
if (active
|
|
650
|
-
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 });
|
|
651
1221
|
}
|
|
652
1222
|
const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
|
|
653
1223
|
active = await this.startNextCycle(active, [summary], [{
|
|
@@ -685,7 +1255,7 @@ export class RunIssue {
|
|
|
685
1255
|
catch {
|
|
686
1256
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
687
1257
|
}
|
|
688
|
-
if (await this.dependencies.git.getHead(worktreePath) !==
|
|
1258
|
+
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
689
1259
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
690
1260
|
}
|
|
691
1261
|
const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
@@ -701,7 +1271,7 @@ export class RunIssue {
|
|
|
701
1271
|
runId,
|
|
702
1272
|
issueNumber: input.issueNumber,
|
|
703
1273
|
cycle: active.record.cycle,
|
|
704
|
-
baseSha,
|
|
1274
|
+
baseSha: expectedImplementationHead(active.record),
|
|
705
1275
|
headSha: freshness.headSha,
|
|
706
1276
|
indexTreeSha: freshness.indexTreeSha,
|
|
707
1277
|
trackedContentSha256: freshness.trackedContentSha256,
|
|
@@ -726,9 +1296,16 @@ export class RunIssue {
|
|
|
726
1296
|
frozenCriteria,
|
|
727
1297
|
checkedChange,
|
|
728
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
|
+
},
|
|
729
1304
|
});
|
|
730
1305
|
}
|
|
731
1306
|
catch (error) {
|
|
1307
|
+
if (error instanceof ProofLaunchAuthorizationError)
|
|
1308
|
+
return error.outcome;
|
|
732
1309
|
if (error instanceof ProofQuiescenceError) {
|
|
733
1310
|
active = await this.persist(active, {
|
|
734
1311
|
lifecycle: 'safe-halt',
|
|
@@ -764,8 +1341,10 @@ export class RunIssue {
|
|
|
764
1341
|
if (this.signal.aborted)
|
|
765
1342
|
return await this.terminal(active, { status: 'cancelled' });
|
|
766
1343
|
if (proof.status === 'needs-rework') {
|
|
767
|
-
if (active
|
|
768
|
-
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 });
|
|
769
1348
|
}
|
|
770
1349
|
active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
|
|
771
1350
|
provenance: 'proof',
|
|
@@ -777,10 +1356,23 @@ export class RunIssue {
|
|
|
777
1356
|
}
|
|
778
1357
|
if (proof.status !== 'passed')
|
|
779
1358
|
return await this.mapProofFailure(active, proof);
|
|
780
|
-
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
|
+
});
|
|
781
1371
|
break;
|
|
782
1372
|
}
|
|
783
|
-
return
|
|
1373
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1374
|
+
? await this.updateExistingPullRequest(active, config, input.issueNumber)
|
|
1375
|
+
: await this.publish(active, config, issueSnapshot, input.issueNumber);
|
|
784
1376
|
}
|
|
785
1377
|
catch (error) {
|
|
786
1378
|
if (!active && error instanceof TransportReadError) {
|
|
@@ -795,11 +1387,11 @@ export class RunIssue {
|
|
|
795
1387
|
if (active && error instanceof TransportReadError) {
|
|
796
1388
|
return await this.invokedFailure(active, 'authorization-read-failed');
|
|
797
1389
|
}
|
|
798
|
-
if (active && error instanceof
|
|
1390
|
+
if (active && error instanceof RouteInitializationUnrecoverableError) {
|
|
799
1391
|
const evidence = await this.dependencies.writeEvidence({
|
|
800
1392
|
runId: active.record.runId,
|
|
801
|
-
code: 'route-
|
|
802
|
-
summary: 'The
|
|
1393
|
+
code: 'route-initialization-unrecoverable',
|
|
1394
|
+
summary: 'The claimed run cannot be safely initialized without product-state ambiguity.',
|
|
803
1395
|
});
|
|
804
1396
|
return { status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path };
|
|
805
1397
|
}
|
|
@@ -830,26 +1422,26 @@ export class RunIssue {
|
|
|
830
1422
|
}
|
|
831
1423
|
}
|
|
832
1424
|
}
|
|
833
|
-
async
|
|
1425
|
+
async initializeClaimedRun(active, issue) {
|
|
834
1426
|
if (active.record.lifecycle !== 'claimed'
|
|
835
1427
|
|| active.record.process
|
|
836
1428
|
|| active.record.intent
|
|
837
1429
|
|| active.record.routeExecution
|
|
838
1430
|
|| active.record.routeReceipt) {
|
|
839
|
-
throw new
|
|
1431
|
+
throw new RouteInitializationUnrecoverableError();
|
|
840
1432
|
}
|
|
841
1433
|
const [snapshot, changedFiles] = await Promise.all([
|
|
842
1434
|
this.dependencies.git.snapshot(active.record.worktreePath),
|
|
843
1435
|
this.dependencies.git.listChangedFiles(active.record.worktreePath),
|
|
844
1436
|
]);
|
|
845
1437
|
if (snapshot.headSha !== active.record.baseSha || changedFiles.length !== 0) {
|
|
846
|
-
throw new
|
|
1438
|
+
throw new RouteInitializationUnrecoverableError();
|
|
847
1439
|
}
|
|
848
1440
|
try {
|
|
849
1441
|
await this.dependencies.verifyWorkflowGeneration(active.record.workflowGeneration);
|
|
850
1442
|
}
|
|
851
1443
|
catch {
|
|
852
|
-
throw new
|
|
1444
|
+
throw new RouteInitializationUnrecoverableError();
|
|
853
1445
|
}
|
|
854
1446
|
const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
|
|
855
1447
|
return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
|
|
@@ -914,7 +1506,15 @@ export class RunIssue {
|
|
|
914
1506
|
if (result.status === 'repairable' || result.status === 'retryable')
|
|
915
1507
|
continue;
|
|
916
1508
|
if (result.status === 'safe-halt') {
|
|
917
|
-
active = await this.persist(active, {
|
|
1509
|
+
active = await this.persist(active, {
|
|
1510
|
+
lifecycle: 'safe-halt',
|
|
1511
|
+
process: {
|
|
1512
|
+
...result.process,
|
|
1513
|
+
purpose: 'route',
|
|
1514
|
+
resumeLifecycle: 'triaging',
|
|
1515
|
+
resumeReviewStage: null,
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
918
1518
|
while (true) {
|
|
919
1519
|
try {
|
|
920
1520
|
await result.waitForAbsence();
|
|
@@ -1166,8 +1766,9 @@ export class RunIssue {
|
|
|
1166
1766
|
const state = await this.dependencies.runRecords.read();
|
|
1167
1767
|
const now = this.timestamp();
|
|
1168
1768
|
const workflow = await this.dependencies.createWorkflowGeneration();
|
|
1769
|
+
const { config, ...persistedInput } = input;
|
|
1169
1770
|
const record = {
|
|
1170
|
-
...
|
|
1771
|
+
...persistedInput,
|
|
1171
1772
|
lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
|
|
1172
1773
|
reworkFindings: [],
|
|
1173
1774
|
packageVersion: workflow.receipt.packageVersion,
|
|
@@ -1176,9 +1777,9 @@ export class RunIssue {
|
|
|
1176
1777
|
checks: [], createdAt: now, updatedAt: now,
|
|
1177
1778
|
};
|
|
1178
1779
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1179
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
1780
|
+
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
1180
1781
|
});
|
|
1181
|
-
return { state: saved, record: findRun(saved, input.runId) };
|
|
1782
|
+
return { state: saved, record: findRun(saved, input.runId), config };
|
|
1182
1783
|
}
|
|
1183
1784
|
async persist(active, changes) {
|
|
1184
1785
|
const record = { ...active.record, ...changes, updatedAt: this.timestamp() };
|
|
@@ -1186,15 +1787,15 @@ export class RunIssue {
|
|
|
1186
1787
|
delete record.intent;
|
|
1187
1788
|
if (Object.hasOwn(changes, 'process') && changes.process === undefined)
|
|
1188
1789
|
delete record.process;
|
|
1189
|
-
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
|
|
1790
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback']) {
|
|
1190
1791
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1191
1792
|
delete record[key];
|
|
1192
1793
|
}
|
|
1193
1794
|
const runs = active.state.runs.map((candidate) => candidate.runId === record.runId ? record : candidate);
|
|
1194
1795
|
const saved = await this.dependencies.runRecords.compareAndSwap(active.state.generation, {
|
|
1195
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
1796
|
+
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs,
|
|
1196
1797
|
});
|
|
1197
|
-
return { state: saved, record: findRun(saved, record.runId) };
|
|
1798
|
+
return { state: saved, record: findRun(saved, record.runId), config: active.config };
|
|
1198
1799
|
}
|
|
1199
1800
|
clearIntent(active) {
|
|
1200
1801
|
return this.persist(active, { intent: undefined });
|
|
@@ -1214,6 +1815,14 @@ export class RunIssue {
|
|
|
1214
1815
|
const labels = new Set(issue.labels);
|
|
1215
1816
|
if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
|
|
1216
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) {
|
|
1217
1826
|
const exactBody = claimComment(runId, issueNumber, branchName);
|
|
1218
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;
|
|
1219
1828
|
const markers = issue.comments.filter((comment) => markerPattern.test(comment.body.split('\n')[0] ?? ''));
|
|
@@ -1235,6 +1844,30 @@ export class RunIssue {
|
|
|
1235
1844
|
throw new TransportReadError();
|
|
1236
1845
|
}
|
|
1237
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
|
+
}
|
|
1238
1871
|
async runImplementation(input) {
|
|
1239
1872
|
try {
|
|
1240
1873
|
return await this.dependencies.implementationAgent.run({
|
|
@@ -1247,7 +1880,7 @@ export class RunIssue {
|
|
|
1247
1880
|
return { kind: 'internal-error' };
|
|
1248
1881
|
}
|
|
1249
1882
|
}
|
|
1250
|
-
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
|
|
1883
|
+
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles, config) {
|
|
1251
1884
|
let active = starting;
|
|
1252
1885
|
let reportRepair;
|
|
1253
1886
|
while (true) {
|
|
@@ -1264,6 +1897,9 @@ export class RunIssue {
|
|
|
1264
1897
|
? directReviewClosureRequestSha256(directReview)
|
|
1265
1898
|
: null;
|
|
1266
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;
|
|
1267
1903
|
const result = await this.dependencies.implementationReviewer.run({
|
|
1268
1904
|
runId: active.record.runId,
|
|
1269
1905
|
worktreePath: active.record.worktreePath,
|
|
@@ -1312,8 +1948,10 @@ export class RunIssue {
|
|
|
1312
1948
|
if (!current)
|
|
1313
1949
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1314
1950
|
if (result.report.verdict === 'needs-work') {
|
|
1315
|
-
if (active
|
|
1316
|
-
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');
|
|
1317
1955
|
}
|
|
1318
1956
|
const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
|
|
1319
1957
|
const findings = [
|
|
@@ -1326,7 +1964,12 @@ export class RunIssue {
|
|
|
1326
1964
|
];
|
|
1327
1965
|
return this.persist(active, {
|
|
1328
1966
|
lifecycle: 'implementing',
|
|
1329
|
-
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
|
+
} : {}),
|
|
1330
1973
|
directReview: repaired,
|
|
1331
1974
|
reworkFindings: findings,
|
|
1332
1975
|
checks: [],
|
|
@@ -1346,16 +1989,14 @@ export class RunIssue {
|
|
|
1346
1989
|
const current = active.record.directReview;
|
|
1347
1990
|
if (!current)
|
|
1348
1991
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1349
|
-
|
|
1350
|
-
if (selected.reportRepairs >= 1 || reportRepair) {
|
|
1992
|
+
if (current.review.reportRepairs >= 1 || reportRepair) {
|
|
1351
1993
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
|
|
1352
1994
|
}
|
|
1353
1995
|
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
1354
|
-
const trackKey = current.stage?.startsWith('cleanup-') ? 'cleanup' : 'review';
|
|
1355
1996
|
active = await this.persist(active, {
|
|
1356
1997
|
directReview: {
|
|
1357
1998
|
...withoutInvocation,
|
|
1358
|
-
|
|
1999
|
+
review: { ...withoutInvocation.review, reportRepairs: 1 },
|
|
1359
2000
|
},
|
|
1360
2001
|
});
|
|
1361
2002
|
reportRepair = {
|
|
@@ -1450,7 +2091,12 @@ export class RunIssue {
|
|
|
1450
2091
|
}));
|
|
1451
2092
|
return this.persist(active, {
|
|
1452
2093
|
lifecycle: 'implementing',
|
|
1453
|
-
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
|
+
} : {}),
|
|
1454
2100
|
reworkFindings: [...findings],
|
|
1455
2101
|
directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
|
|
1456
2102
|
checks: [],
|
|
@@ -1462,13 +2108,81 @@ export class RunIssue {
|
|
|
1462
2108
|
active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
|
|
1463
2109
|
return this.persist(active, {
|
|
1464
2110
|
lifecycle: 'implementing',
|
|
1465
|
-
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
|
+
} : {}),
|
|
1466
2117
|
checks: [],
|
|
1467
2118
|
checkedChangeSha256: undefined,
|
|
1468
2119
|
proofId: undefined,
|
|
1469
2120
|
proofReceipt: undefined,
|
|
1470
2121
|
});
|
|
1471
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
|
+
}
|
|
1472
2186
|
async mapImplementationFailure(active, result) {
|
|
1473
2187
|
if (result.kind === 'transport-failed')
|
|
1474
2188
|
return this.terminal(active, { status: 'transport-failed', resumable: result.resumable });
|
|
@@ -1495,6 +2209,9 @@ export class RunIssue {
|
|
|
1495
2209
|
return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
|
|
1496
2210
|
}
|
|
1497
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
|
+
}
|
|
1498
2215
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
|
|
1499
2216
|
const terminalOutcome = { ...outcome, evidencePath: evidence.path };
|
|
1500
2217
|
const changes = {
|
|
@@ -1696,6 +2413,9 @@ function sameFreshness(left, right) {
|
|
|
1696
2413
|
&& left.untrackedContentSha256 === right.untrackedContentSha256
|
|
1697
2414
|
&& left.worktreeIdentity === right.worktreeIdentity;
|
|
1698
2415
|
}
|
|
2416
|
+
function expectedImplementationHead(record) {
|
|
2417
|
+
return record.reviewFeedback?.activeBatch?.priorPublishedHeadSha ?? record.baseSha;
|
|
2418
|
+
}
|
|
1699
2419
|
function commentsWithMarker(issue, marker) {
|
|
1700
2420
|
return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
1701
2421
|
}
|