codex-orchestrator 2.0.4 → 2.0.6
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 +50 -0
- package/README.md +30 -3
- package/dist/src/v2/acceptance-proof.d.ts +12 -0
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +120 -6
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/command.d.ts +2 -0
- package/dist/src/v2/adapters/command.d.ts.map +1 -1
- package/dist/src/v2/adapters/command.js +67 -8
- package/dist/src/v2/adapters/command.js.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.js +16 -5
- 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/android-proof-runner.d.ts +93 -0
- package/dist/src/v2/android-proof-runner.d.ts.map +1 -0
- package/dist/src/v2/android-proof-runner.js +921 -0
- package/dist/src/v2/android-proof-runner.js.map +1 -0
- 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 +13 -1
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +57 -4
- 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/immutable-workflow-publisher.js +6 -1
- package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
- package/dist/src/v2/mobile-lease.d.ts +9 -0
- package/dist/src/v2/mobile-lease.d.ts.map +1 -1
- package/dist/src/v2/mobile-lease.js +27 -3
- package/dist/src/v2/mobile-lease.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 +61 -0
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +843 -66
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +48 -1
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +125 -5
- 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 +183 -15
- 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 +71 -7
- 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/acceptance-proof/SKILL.md +1 -1
- package/internal-workflow/skills/acceptance-proof/references/android.md +6 -6
- 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/SKILL.md +10 -8
- package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +17 -3
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +30 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +30 -6
- 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/internal-workflow/skills/acceptance-proof/tools/android-lease.mjs +0 -280
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 {
|
|
@@ -103,7 +104,7 @@ export class RunIssue {
|
|
|
103
104
|
return await this.publicationDiverged(active, 'commit-tree-diverged');
|
|
104
105
|
if (this.signal.aborted)
|
|
105
106
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
106
|
-
if (!await this.authorized(
|
|
107
|
+
if (!await this.authorized(active, config))
|
|
107
108
|
return await this.revoked(active);
|
|
108
109
|
try {
|
|
109
110
|
commitSha = await this.dependencies.git.commit({ worktreePath, message: intent.message });
|
|
@@ -135,7 +136,7 @@ export class RunIssue {
|
|
|
135
136
|
if (!remoteSha) {
|
|
136
137
|
if (this.signal.aborted)
|
|
137
138
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
138
|
-
if (!await this.authorized(
|
|
139
|
+
if (!await this.authorized(active, config))
|
|
139
140
|
return await this.revoked(active);
|
|
140
141
|
try {
|
|
141
142
|
await this.dependencies.git.push({ worktreePath, branchName });
|
|
@@ -169,7 +170,7 @@ export class RunIssue {
|
|
|
169
170
|
if (!observed) {
|
|
170
171
|
if (this.signal.aborted)
|
|
171
172
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
172
|
-
if (!await this.authorized(
|
|
173
|
+
if (!await this.authorized(active, config))
|
|
173
174
|
return await this.revoked(active);
|
|
174
175
|
try {
|
|
175
176
|
await this.dependencies.pullRequests.createDraft({
|
|
@@ -209,7 +210,7 @@ export class RunIssue {
|
|
|
209
210
|
if (matching.length === 0) {
|
|
210
211
|
if (this.signal.aborted)
|
|
211
212
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
212
|
-
if (!await this.authorized(
|
|
213
|
+
if (!await this.authorized(active, config))
|
|
213
214
|
return await this.revoked(active);
|
|
214
215
|
try {
|
|
215
216
|
await this.dependencies.issues.postComment(issueNumber, handoffBody);
|
|
@@ -235,7 +236,7 @@ export class RunIssue {
|
|
|
235
236
|
if (!sameStrings(observation.labels, terminalLabels)) {
|
|
236
237
|
if (this.signal.aborted)
|
|
237
238
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
238
|
-
if (!await this.authorized(
|
|
239
|
+
if (!await this.authorized(active, config))
|
|
239
240
|
return await this.revoked(active);
|
|
240
241
|
try {
|
|
241
242
|
await this.dependencies.issues.setLabels(issueNumber, terminalLabels);
|
|
@@ -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(active, 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(active, 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(active, 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(active, 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, [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);
|
|
599
|
+
const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, reviewLabels)
|
|
600
|
+
&& this.hasTrustedClaim(issue, active.record);
|
|
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)) {
|
|
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,32 +743,58 @@ 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);
|
|
352
779
|
if ('result' in claim)
|
|
353
780
|
return claim.result;
|
|
354
781
|
active = claim.active;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
782
|
+
let worktree;
|
|
783
|
+
try {
|
|
784
|
+
worktree = await this.dependencies.git.inspectWorktree({ worktreePath, branchName, baseSha });
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
return await this.invokedFailure(active, 'local-git-worktree-inspection-failed', claimedWorktreeInspectionFailureSummary(error));
|
|
788
|
+
}
|
|
789
|
+
if (worktree === 'diverged') {
|
|
790
|
+
return await this.invokedFailure(active, 'local-git-worktree-diverged', 'The claimed worktree path is present but does not match the expected branch and base. Correct or preserve the local artifact, then retry the same run.');
|
|
791
|
+
}
|
|
358
792
|
if (worktree === 'absent') {
|
|
359
793
|
try {
|
|
360
794
|
await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
|
|
361
795
|
}
|
|
362
|
-
catch {
|
|
363
|
-
return await this.
|
|
796
|
+
catch (error) {
|
|
797
|
+
return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
|
|
364
798
|
}
|
|
365
799
|
}
|
|
366
800
|
active = await this.initializeClaimedRun(active, issue);
|
|
@@ -388,14 +822,45 @@ export class RunIssue {
|
|
|
388
822
|
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
389
823
|
}
|
|
390
824
|
if (existing.lifecycle === 'spec-authoring') {
|
|
391
|
-
if (!await this.authorized(
|
|
825
|
+
if (!await this.authorized(active, config))
|
|
392
826
|
return await this.revoked(active);
|
|
393
827
|
return await this.continueSpecRequired(active);
|
|
394
828
|
}
|
|
829
|
+
const feedbackInvocation = active.record.reviewFeedback?.implementationInvocation;
|
|
830
|
+
if (feedbackInvocation?.phase === 'prepared') {
|
|
831
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-launch-ambiguous');
|
|
832
|
+
}
|
|
833
|
+
if (feedbackInvocation?.phase === 'launched') {
|
|
834
|
+
try {
|
|
835
|
+
await this.dependencies.waitForReviewProcessAbsence(feedbackInvocation.processGroupId);
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-process-absence-unconfirmed');
|
|
839
|
+
}
|
|
840
|
+
if (!this.dependencies.implementationAgent.recover) {
|
|
841
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-unavailable');
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
recoveredFeedbackImplementation = await this.dependencies.implementationAgent.recover({
|
|
845
|
+
runId: active.record.runId,
|
|
846
|
+
canonicalRepository: active.record.canonicalRepository,
|
|
847
|
+
invocation: feedbackInvocation,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-failed');
|
|
852
|
+
}
|
|
853
|
+
if (!recoveredFeedbackImplementation) {
|
|
854
|
+
const snapshot = await this.dependencies.git.snapshot(active.record.worktreePath);
|
|
855
|
+
if (!sameFreshness(snapshot, feedbackInvocation.baseline)) {
|
|
856
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
395
860
|
if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
396
861
|
return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
|
|
397
862
|
}
|
|
398
|
-
if (!await this.authorized(
|
|
863
|
+
if (!await this.authorized(active, config))
|
|
399
864
|
return await this.revoked(active);
|
|
400
865
|
if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
|
|
401
866
|
if (!active.record.routeExecution || !active.record.routeReceipt)
|
|
@@ -404,8 +869,12 @@ export class RunIssue {
|
|
|
404
869
|
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
|
|
405
870
|
const checkRecovery = active.record.lifecycle === 'checking'
|
|
406
871
|
&& active.record.directReview?.status === 'clear';
|
|
407
|
-
|
|
408
|
-
|
|
872
|
+
const proofRecovery = active.record.lifecycle === 'proving'
|
|
873
|
+
&& active.record.directReview?.status === 'clear';
|
|
874
|
+
const feedbackRepair = active.record.reviewFeedback?.phase === 'repairing'
|
|
875
|
+
&& active.record.directReview?.stage === 'review-repair';
|
|
876
|
+
if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair) {
|
|
877
|
+
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
409
878
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
410
879
|
}
|
|
411
880
|
active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
|
|
@@ -430,7 +899,7 @@ export class RunIssue {
|
|
|
430
899
|
const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
|
|
431
900
|
active = await this.createRun({
|
|
432
901
|
runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
|
|
433
|
-
issueSnapshot, frozenCriteria,
|
|
902
|
+
issueSnapshot, frozenCriteria, config,
|
|
434
903
|
intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
|
|
435
904
|
});
|
|
436
905
|
const claim = await this.reconcileClaim(active, config);
|
|
@@ -440,8 +909,8 @@ export class RunIssue {
|
|
|
440
909
|
try {
|
|
441
910
|
await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
|
|
442
911
|
}
|
|
443
|
-
catch {
|
|
444
|
-
return await this.
|
|
912
|
+
catch (error) {
|
|
913
|
+
return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
|
|
445
914
|
}
|
|
446
915
|
active = await this.initializeClaimedRun(active, issue);
|
|
447
916
|
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
@@ -452,19 +921,19 @@ export class RunIssue {
|
|
|
452
921
|
return routed.result;
|
|
453
922
|
active = routed.active;
|
|
454
923
|
}
|
|
455
|
-
if (
|
|
924
|
+
if (!['implementing', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
456
925
|
return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
|
|
457
926
|
}
|
|
458
927
|
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
459
928
|
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
|
|
460
929
|
}
|
|
461
930
|
attemptLoop: while (true) {
|
|
462
|
-
if (!await this.authorized(
|
|
931
|
+
if (!await this.authorized(active, config)) {
|
|
463
932
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
464
933
|
}
|
|
465
934
|
if (this.signal.aborted)
|
|
466
935
|
return await this.terminal(active, { status: 'cancelled' });
|
|
467
|
-
let resumeAtChecks = active.record.lifecycle === 'checking'
|
|
936
|
+
let resumeAtChecks = (active.record.lifecycle === 'checking' || active.record.lifecycle === 'proving')
|
|
468
937
|
&& active.record.directReview?.status === 'clear';
|
|
469
938
|
if (active.record.lifecycle === 'implementing'
|
|
470
939
|
&& active.record.directReview?.status === 'active'
|
|
@@ -473,7 +942,7 @@ export class RunIssue {
|
|
|
473
942
|
if ('status' in recovered)
|
|
474
943
|
return recovered;
|
|
475
944
|
active = recovered;
|
|
476
|
-
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
945
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
|
|
477
946
|
if ('status' in reviewed)
|
|
478
947
|
return reviewed;
|
|
479
948
|
active = reviewed;
|
|
@@ -482,8 +951,40 @@ export class RunIssue {
|
|
|
482
951
|
resumeAtChecks = true;
|
|
483
952
|
}
|
|
484
953
|
if (!resumeAtChecks) {
|
|
954
|
+
const feedbackBatch = active.record.reviewFeedback?.activeBatch;
|
|
955
|
+
const workerBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
956
|
+
if (workerBlock)
|
|
957
|
+
return workerBlock;
|
|
958
|
+
const feedbackProjection = feedbackBatch
|
|
959
|
+
? projectReviewFeedbackBatch(feedbackBatch, active.record.directReview.targetRevision)
|
|
960
|
+
: undefined;
|
|
961
|
+
const feedbackLaunch = feedbackBatch ? {
|
|
962
|
+
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
963
|
+
reviewFeedback: feedbackProjection.workerFeedback,
|
|
964
|
+
onPrepared: async (invocation) => {
|
|
965
|
+
const currentActive = active;
|
|
966
|
+
active = await this.persist(currentActive, {
|
|
967
|
+
reviewFeedback: {
|
|
968
|
+
...structuredClone(currentActive.record.reviewFeedback),
|
|
969
|
+
implementationInvocation: { ...structuredClone(invocation), phase: 'prepared', pid: null, processGroupId: null, launchedAt: null },
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
},
|
|
973
|
+
onLaunched: async (invocation) => {
|
|
974
|
+
const currentActive = active;
|
|
975
|
+
const current = currentActive.record.reviewFeedback?.implementationInvocation;
|
|
976
|
+
if (!current || current.phase !== 'prepared' || current.attemptId !== invocation.attemptId)
|
|
977
|
+
throw new Error('review feedback implementation launch mismatch');
|
|
978
|
+
active = await this.persist(currentActive, {
|
|
979
|
+
reviewFeedback: {
|
|
980
|
+
...structuredClone(currentActive.record.reviewFeedback),
|
|
981
|
+
implementationInvocation: { ...current, phase: 'launched', pid: invocation.pid, processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt },
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
},
|
|
985
|
+
} : {};
|
|
485
986
|
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
486
|
-
let implementation = await this.runImplementation({
|
|
987
|
+
let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
|
|
487
988
|
runId,
|
|
488
989
|
worktreePath,
|
|
489
990
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -492,7 +993,9 @@ export class RunIssue {
|
|
|
492
993
|
reworkFindings: active.record.reworkFindings,
|
|
493
994
|
repairOnly: false,
|
|
494
995
|
workflowGeneration: active.record.workflowGeneration,
|
|
996
|
+
...feedbackLaunch,
|
|
495
997
|
});
|
|
998
|
+
recoveredFeedbackImplementation = undefined;
|
|
496
999
|
if (implementation.kind === 'safe-halt') {
|
|
497
1000
|
active = await this.persist(active, {
|
|
498
1001
|
lifecycle: 'safe-halt',
|
|
@@ -521,6 +1024,10 @@ export class RunIssue {
|
|
|
521
1024
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
522
1025
|
}
|
|
523
1026
|
if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
|
|
1027
|
+
const invocation = active.record.reviewFeedback?.implementationInvocation;
|
|
1028
|
+
if (invocation && !sameFreshness(await this.dependencies.git.snapshot(worktreePath), invocation.baseline)) {
|
|
1029
|
+
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
|
|
1030
|
+
}
|
|
524
1031
|
active = await this.persist(active, { transportRetries: 1 });
|
|
525
1032
|
continue attemptLoop;
|
|
526
1033
|
}
|
|
@@ -536,6 +1043,9 @@ export class RunIssue {
|
|
|
536
1043
|
}
|
|
537
1044
|
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
538
1045
|
active = await this.persist(active, { reportRepairs: 1 });
|
|
1046
|
+
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1047
|
+
if (repairBlock)
|
|
1048
|
+
return repairBlock;
|
|
539
1049
|
implementation = await this.runImplementation({
|
|
540
1050
|
runId,
|
|
541
1051
|
worktreePath,
|
|
@@ -545,6 +1055,7 @@ export class RunIssue {
|
|
|
545
1055
|
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
546
1056
|
repairOnly: true,
|
|
547
1057
|
workflowGeneration: active.record.workflowGeneration,
|
|
1058
|
+
...feedbackLaunch,
|
|
548
1059
|
});
|
|
549
1060
|
if (implementation.kind === 'safe-halt') {
|
|
550
1061
|
active = await this.persist(active, {
|
|
@@ -583,7 +1094,7 @@ export class RunIssue {
|
|
|
583
1094
|
if (report.status === 'external-block') {
|
|
584
1095
|
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
585
1096
|
}
|
|
586
|
-
if (await this.dependencies.git.getHead(worktreePath) !==
|
|
1097
|
+
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
587
1098
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
588
1099
|
}
|
|
589
1100
|
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
@@ -593,6 +1104,9 @@ export class RunIssue {
|
|
|
593
1104
|
}
|
|
594
1105
|
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
595
1106
|
active = await this.persist(active, { reportRepairs: 1 });
|
|
1107
|
+
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1108
|
+
if (repairBlock)
|
|
1109
|
+
return repairBlock;
|
|
596
1110
|
implementation = await this.runImplementation({
|
|
597
1111
|
runId,
|
|
598
1112
|
worktreePath,
|
|
@@ -602,6 +1116,7 @@ export class RunIssue {
|
|
|
602
1116
|
reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
|
|
603
1117
|
repairOnly: true,
|
|
604
1118
|
workflowGeneration: active.record.workflowGeneration,
|
|
1119
|
+
...feedbackLaunch,
|
|
605
1120
|
});
|
|
606
1121
|
if (implementation.kind === 'safe-halt') {
|
|
607
1122
|
active = await this.persist(active, {
|
|
@@ -655,6 +1170,9 @@ export class RunIssue {
|
|
|
655
1170
|
if (active.record.directReview?.stage === 'review-repair') {
|
|
656
1171
|
active = await this.persist(active, {
|
|
657
1172
|
directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
|
|
1173
|
+
...(active.record.reviewFeedback ? {
|
|
1174
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1175
|
+
} : {}),
|
|
658
1176
|
});
|
|
659
1177
|
}
|
|
660
1178
|
else {
|
|
@@ -665,9 +1183,12 @@ export class RunIssue {
|
|
|
665
1183
|
targetFingerprint,
|
|
666
1184
|
codeReviewerSessionId: reviewerSessionId,
|
|
667
1185
|
}),
|
|
1186
|
+
...(active.record.reviewFeedback ? {
|
|
1187
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1188
|
+
} : {}),
|
|
668
1189
|
});
|
|
669
1190
|
}
|
|
670
|
-
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
1191
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
|
|
671
1192
|
if ('status' in reviewed)
|
|
672
1193
|
return reviewed;
|
|
673
1194
|
active = reviewed;
|
|
@@ -675,7 +1196,12 @@ export class RunIssue {
|
|
|
675
1196
|
continue attemptLoop;
|
|
676
1197
|
}
|
|
677
1198
|
else {
|
|
678
|
-
active = await this.persist(active, {
|
|
1199
|
+
active = await this.persist(active, {
|
|
1200
|
+
lifecycle: 'checking',
|
|
1201
|
+
...(active.record.reviewFeedback ? {
|
|
1202
|
+
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1203
|
+
} : {}),
|
|
1204
|
+
});
|
|
679
1205
|
}
|
|
680
1206
|
}
|
|
681
1207
|
for (const [id, command] of Object.entries(config.checks)) {
|
|
@@ -695,8 +1221,10 @@ export class RunIssue {
|
|
|
695
1221
|
const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
|
|
696
1222
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
697
1223
|
if (check.status !== 'passed') {
|
|
698
|
-
if (active
|
|
699
|
-
return
|
|
1224
|
+
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1225
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1226
|
+
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
1227
|
+
: await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
700
1228
|
}
|
|
701
1229
|
const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
|
|
702
1230
|
active = await this.startNextCycle(active, [summary], [{
|
|
@@ -734,7 +1262,7 @@ export class RunIssue {
|
|
|
734
1262
|
catch {
|
|
735
1263
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
736
1264
|
}
|
|
737
|
-
if (await this.dependencies.git.getHead(worktreePath) !==
|
|
1265
|
+
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
738
1266
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
739
1267
|
}
|
|
740
1268
|
const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
@@ -750,7 +1278,7 @@ export class RunIssue {
|
|
|
750
1278
|
runId,
|
|
751
1279
|
issueNumber: input.issueNumber,
|
|
752
1280
|
cycle: active.record.cycle,
|
|
753
|
-
baseSha,
|
|
1281
|
+
baseSha: expectedImplementationHead(active.record),
|
|
754
1282
|
headSha: freshness.headSha,
|
|
755
1283
|
indexTreeSha: freshness.indexTreeSha,
|
|
756
1284
|
trackedContentSha256: freshness.trackedContentSha256,
|
|
@@ -775,9 +1303,16 @@ export class RunIssue {
|
|
|
775
1303
|
frozenCriteria,
|
|
776
1304
|
checkedChange,
|
|
777
1305
|
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1306
|
+
beforeAgentLaunch: async () => {
|
|
1307
|
+
const failure = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1308
|
+
if (failure)
|
|
1309
|
+
throw new ProofLaunchAuthorizationError(failure);
|
|
1310
|
+
},
|
|
778
1311
|
});
|
|
779
1312
|
}
|
|
780
1313
|
catch (error) {
|
|
1314
|
+
if (error instanceof ProofLaunchAuthorizationError)
|
|
1315
|
+
return error.outcome;
|
|
781
1316
|
if (error instanceof ProofQuiescenceError) {
|
|
782
1317
|
active = await this.persist(active, {
|
|
783
1318
|
lifecycle: 'safe-halt',
|
|
@@ -813,8 +1348,10 @@ export class RunIssue {
|
|
|
813
1348
|
if (this.signal.aborted)
|
|
814
1349
|
return await this.terminal(active, { status: 'cancelled' });
|
|
815
1350
|
if (proof.status === 'needs-rework') {
|
|
816
|
-
if (active
|
|
817
|
-
return
|
|
1351
|
+
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1352
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1353
|
+
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-proof-repair-exhausted')
|
|
1354
|
+
: await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
818
1355
|
}
|
|
819
1356
|
active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
|
|
820
1357
|
provenance: 'proof',
|
|
@@ -826,10 +1363,23 @@ export class RunIssue {
|
|
|
826
1363
|
}
|
|
827
1364
|
if (proof.status !== 'passed')
|
|
828
1365
|
return await this.mapProofFailure(active, proof);
|
|
829
|
-
active = await this.persist(active, {
|
|
1366
|
+
active = await this.persist(active, {
|
|
1367
|
+
lifecycle: 'publishing',
|
|
1368
|
+
proofReceipt: proof.receipt,
|
|
1369
|
+
reworkFindings: [],
|
|
1370
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
1371
|
+
reviewFeedback: markReviewFeedbackPublishing(markReviewFeedbackVerified(active.record.reviewFeedback, {
|
|
1372
|
+
checkedChangeSha256,
|
|
1373
|
+
proofId,
|
|
1374
|
+
verifiedAt: this.timestamp(),
|
|
1375
|
+
})),
|
|
1376
|
+
} : {}),
|
|
1377
|
+
});
|
|
830
1378
|
break;
|
|
831
1379
|
}
|
|
832
|
-
return
|
|
1380
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1381
|
+
? await this.updateExistingPullRequest(active, config, input.issueNumber)
|
|
1382
|
+
: await this.publish(active, config, issueSnapshot, input.issueNumber);
|
|
833
1383
|
}
|
|
834
1384
|
catch (error) {
|
|
835
1385
|
if (!active && error instanceof TransportReadError) {
|
|
@@ -900,12 +1450,31 @@ export class RunIssue {
|
|
|
900
1450
|
catch {
|
|
901
1451
|
throw new RouteInitializationUnrecoverableError();
|
|
902
1452
|
}
|
|
903
|
-
const issueSnapshot = issue?.state === 'OPEN'
|
|
1453
|
+
const issueSnapshot = issue?.state === 'OPEN'
|
|
1454
|
+
? refreshClaimedIssueSnapshot(active.record.issueSnapshot, issue)
|
|
1455
|
+
: active.record.issueSnapshot;
|
|
904
1456
|
return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
|
|
905
1457
|
}
|
|
906
1458
|
async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
|
|
907
1459
|
let active = starting;
|
|
908
1460
|
while (active.record.lifecycle === 'triaging') {
|
|
1461
|
+
let currentIssue;
|
|
1462
|
+
try {
|
|
1463
|
+
currentIssue = await this.readIssue(active.record.issueNumber);
|
|
1464
|
+
}
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
if (error instanceof TransportReadError) {
|
|
1467
|
+
return { result: await this.invokedFailure(active, 'authorization-read-failed') };
|
|
1468
|
+
}
|
|
1469
|
+
throw error;
|
|
1470
|
+
}
|
|
1471
|
+
if (!currentIssue || !this.isAuthorizedIssue(currentIssue, active.record, config)) {
|
|
1472
|
+
return { result: await this.revoked(active) };
|
|
1473
|
+
}
|
|
1474
|
+
active = await this.persist(active, {
|
|
1475
|
+
issueSnapshot: refreshClaimedIssueSnapshot(active.record.issueSnapshot, currentIssue),
|
|
1476
|
+
});
|
|
1477
|
+
issue = structuredClone(active.record.issueSnapshot);
|
|
909
1478
|
const state = {
|
|
910
1479
|
read: async () => requireRouteExecution(active.record.routeExecution),
|
|
911
1480
|
compareAndSwap: async (expected, next) => {
|
|
@@ -1007,7 +1576,7 @@ export class RunIssue {
|
|
|
1007
1576
|
return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
|
|
1008
1577
|
}
|
|
1009
1578
|
try {
|
|
1010
|
-
if (!await this.authorized(
|
|
1579
|
+
if (!await this.authorized(active, config)) {
|
|
1011
1580
|
return { result: await this.revoked(active) };
|
|
1012
1581
|
}
|
|
1013
1582
|
}
|
|
@@ -1032,7 +1601,7 @@ export class RunIssue {
|
|
|
1032
1601
|
if (receipt.route !== 'awaiting-user')
|
|
1033
1602
|
active = await this.persist(active, { lifecycle });
|
|
1034
1603
|
try {
|
|
1035
|
-
if (!await this.authorized(
|
|
1604
|
+
if (!await this.authorized(active, config)) {
|
|
1036
1605
|
return { result: await this.revoked(active) };
|
|
1037
1606
|
}
|
|
1038
1607
|
}
|
|
@@ -1223,8 +1792,9 @@ export class RunIssue {
|
|
|
1223
1792
|
const state = await this.dependencies.runRecords.read();
|
|
1224
1793
|
const now = this.timestamp();
|
|
1225
1794
|
const workflow = await this.dependencies.createWorkflowGeneration();
|
|
1795
|
+
const { config, ...persistedInput } = input;
|
|
1226
1796
|
const record = {
|
|
1227
|
-
...
|
|
1797
|
+
...persistedInput,
|
|
1228
1798
|
lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
|
|
1229
1799
|
reworkFindings: [],
|
|
1230
1800
|
packageVersion: workflow.receipt.packageVersion,
|
|
@@ -1233,9 +1803,9 @@ export class RunIssue {
|
|
|
1233
1803
|
checks: [], createdAt: now, updatedAt: now,
|
|
1234
1804
|
};
|
|
1235
1805
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1236
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
1806
|
+
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
1237
1807
|
});
|
|
1238
|
-
return { state: saved, record: findRun(saved, input.runId) };
|
|
1808
|
+
return { state: saved, record: findRun(saved, input.runId), config };
|
|
1239
1809
|
}
|
|
1240
1810
|
async persist(active, changes) {
|
|
1241
1811
|
const record = { ...active.record, ...changes, updatedAt: this.timestamp() };
|
|
@@ -1243,15 +1813,15 @@ export class RunIssue {
|
|
|
1243
1813
|
delete record.intent;
|
|
1244
1814
|
if (Object.hasOwn(changes, 'process') && changes.process === undefined)
|
|
1245
1815
|
delete record.process;
|
|
1246
|
-
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
|
|
1816
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback']) {
|
|
1247
1817
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1248
1818
|
delete record[key];
|
|
1249
1819
|
}
|
|
1250
1820
|
const runs = active.state.runs.map((candidate) => candidate.runId === record.runId ? record : candidate);
|
|
1251
1821
|
const saved = await this.dependencies.runRecords.compareAndSwap(active.state.generation, {
|
|
1252
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
1822
|
+
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs,
|
|
1253
1823
|
});
|
|
1254
|
-
return { state: saved, record: findRun(saved, record.runId) };
|
|
1824
|
+
return { state: saved, record: findRun(saved, record.runId), config: active.config };
|
|
1255
1825
|
}
|
|
1256
1826
|
clearIntent(active) {
|
|
1257
1827
|
return this.persist(active, { intent: undefined });
|
|
@@ -1264,25 +1834,47 @@ export class RunIssue {
|
|
|
1264
1834
|
throw new PostEffectStateError(active);
|
|
1265
1835
|
}
|
|
1266
1836
|
}
|
|
1267
|
-
async authorized(
|
|
1268
|
-
const issue = await this.readIssue(issueNumber);
|
|
1269
|
-
|
|
1837
|
+
async authorized(active, config) {
|
|
1838
|
+
const issue = await this.readIssue(active.record.issueNumber);
|
|
1839
|
+
return !!issue && this.isAuthorizedIssue(issue, active.record, config);
|
|
1840
|
+
}
|
|
1841
|
+
isAuthorizedIssue(issue, record, config) {
|
|
1842
|
+
if (issue.state !== 'OPEN')
|
|
1270
1843
|
return false;
|
|
1271
1844
|
const labels = new Set(issue.labels);
|
|
1272
1845
|
if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
|
|
1273
1846
|
return false;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1847
|
+
return this.hasTrustedClaim(issue, record);
|
|
1848
|
+
}
|
|
1849
|
+
async authorizedForExactLabels(active, expectedLabels) {
|
|
1850
|
+
const issue = await this.readIssue(active.record.issueNumber);
|
|
1851
|
+
return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
|
|
1852
|
+
&& this.hasTrustedClaim(issue, active.record);
|
|
1853
|
+
}
|
|
1854
|
+
hasTrustedClaim(issue, record) {
|
|
1855
|
+
const exactBody = claimComment(record.runId, record.issueNumber, record.branchName);
|
|
1856
|
+
const markers = issue.comments.filter(isClaimMarkerComment);
|
|
1277
1857
|
if (issue.comments.some((comment) => {
|
|
1278
1858
|
const firstLine = comment.body.split('\n')[0] ?? '';
|
|
1279
|
-
return firstLine.startsWith(`<!-- codex-orchestrator:run:${runId}:claim`) && !
|
|
1859
|
+
return firstLine.startsWith(`<!-- codex-orchestrator:run:${record.runId}:claim`) && !claimMarkerPattern.test(firstLine);
|
|
1280
1860
|
}))
|
|
1281
1861
|
return false;
|
|
1282
|
-
|
|
1862
|
+
const currentMarkers = markers.filter((comment) => claimRunId(comment) === record.runId);
|
|
1863
|
+
if (currentMarkers.some((comment) => comment.body !== exactBody
|
|
1864
|
+
|| !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation)))
|
|
1283
1865
|
return false;
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1866
|
+
const historicalClaims = historicalClaimCounts(record);
|
|
1867
|
+
for (const comment of markers) {
|
|
1868
|
+
const markerRunId = claimRunId(comment);
|
|
1869
|
+
if (markerRunId === record.runId)
|
|
1870
|
+
continue;
|
|
1871
|
+
const key = observedHistoricalClaimKeys(comment, record)
|
|
1872
|
+
.find((candidate) => (historicalClaims.get(candidate) ?? 0) > 0);
|
|
1873
|
+
if (!key)
|
|
1874
|
+
return false;
|
|
1875
|
+
historicalClaims.set(key, historicalClaims.get(key) - 1);
|
|
1876
|
+
}
|
|
1877
|
+
return currentMarkers.length === 1;
|
|
1286
1878
|
}
|
|
1287
1879
|
async readIssue(issueNumber) {
|
|
1288
1880
|
try {
|
|
@@ -1292,6 +1884,30 @@ export class RunIssue {
|
|
|
1292
1884
|
throw new TransportReadError();
|
|
1293
1885
|
}
|
|
1294
1886
|
}
|
|
1887
|
+
async revalidateFeedbackWorker(active, config, issueNumber) {
|
|
1888
|
+
const batch = active.record.reviewFeedback?.activeBatch;
|
|
1889
|
+
if (!batch)
|
|
1890
|
+
return undefined;
|
|
1891
|
+
if (!await this.authorized(active, config)) {
|
|
1892
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-authority-revoked');
|
|
1893
|
+
}
|
|
1894
|
+
if (!this.dependencies.reviewFeedback) {
|
|
1895
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-revalidation-unavailable');
|
|
1896
|
+
}
|
|
1897
|
+
const validation = await this.dependencies.reviewFeedback.revalidate({
|
|
1898
|
+
batch,
|
|
1899
|
+
epoch: 'pre-update',
|
|
1900
|
+
expectedHeadSha: batch.priorPublishedHeadSha,
|
|
1901
|
+
});
|
|
1902
|
+
return this.mapFeedbackRevalidation(active, validation, 'review-feedback-worker-revalidation-failed');
|
|
1903
|
+
}
|
|
1904
|
+
async mapFeedbackRevalidation(active, validation, safetyCode) {
|
|
1905
|
+
if (validation.status === 'valid')
|
|
1906
|
+
return undefined;
|
|
1907
|
+
if (validation.status === 'retryable')
|
|
1908
|
+
return this.invokedFailure(active, `${safetyCode}-retryable`);
|
|
1909
|
+
return this.blockReviewFeedback(active, 'safety', safetyCode);
|
|
1910
|
+
}
|
|
1295
1911
|
async runImplementation(input) {
|
|
1296
1912
|
try {
|
|
1297
1913
|
return await this.dependencies.implementationAgent.run({
|
|
@@ -1304,7 +1920,7 @@ export class RunIssue {
|
|
|
1304
1920
|
return { kind: 'internal-error' };
|
|
1305
1921
|
}
|
|
1306
1922
|
}
|
|
1307
|
-
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
|
|
1923
|
+
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles, config) {
|
|
1308
1924
|
let active = starting;
|
|
1309
1925
|
let reportRepair;
|
|
1310
1926
|
while (true) {
|
|
@@ -1321,6 +1937,9 @@ export class RunIssue {
|
|
|
1321
1937
|
? directReviewClosureRequestSha256(directReview)
|
|
1322
1938
|
: null;
|
|
1323
1939
|
const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
|
|
1940
|
+
const reviewerAuthorizationFailure = await this.revalidateFeedbackWorker(active, config, active.record.issueNumber);
|
|
1941
|
+
if (reviewerAuthorizationFailure)
|
|
1942
|
+
return reviewerAuthorizationFailure;
|
|
1324
1943
|
const result = await this.dependencies.implementationReviewer.run({
|
|
1325
1944
|
runId: active.record.runId,
|
|
1326
1945
|
worktreePath: active.record.worktreePath,
|
|
@@ -1369,8 +1988,10 @@ export class RunIssue {
|
|
|
1369
1988
|
if (!current)
|
|
1370
1989
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1371
1990
|
if (result.report.verdict === 'needs-work') {
|
|
1372
|
-
if (active
|
|
1373
|
-
return
|
|
1991
|
+
if (this.repairBudgetExhausted(active, maxCycles)) {
|
|
1992
|
+
return active.record.reviewFeedback?.activeBatch
|
|
1993
|
+
? this.blockReviewFeedback(active, 'exhausted', 'direct-review-repair-exhausted')
|
|
1994
|
+
: this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
|
|
1374
1995
|
}
|
|
1375
1996
|
const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
|
|
1376
1997
|
const findings = [
|
|
@@ -1383,7 +2004,12 @@ export class RunIssue {
|
|
|
1383
2004
|
];
|
|
1384
2005
|
return this.persist(active, {
|
|
1385
2006
|
lifecycle: 'implementing',
|
|
1386
|
-
cycle:
|
|
2007
|
+
cycle: active.record.reviewFeedback?.activeBatch
|
|
2008
|
+
? active.record.cycle
|
|
2009
|
+
: (active.record.cycle + 1),
|
|
2010
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2011
|
+
reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
|
|
2012
|
+
} : {}),
|
|
1387
2013
|
directReview: repaired,
|
|
1388
2014
|
reworkFindings: findings,
|
|
1389
2015
|
checks: [],
|
|
@@ -1505,7 +2131,12 @@ export class RunIssue {
|
|
|
1505
2131
|
}));
|
|
1506
2132
|
return this.persist(active, {
|
|
1507
2133
|
lifecycle: 'implementing',
|
|
1508
|
-
cycle:
|
|
2134
|
+
cycle: active.record.reviewFeedback?.activeBatch
|
|
2135
|
+
? active.record.cycle
|
|
2136
|
+
: (active.record.cycle + 1),
|
|
2137
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2138
|
+
reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
|
|
2139
|
+
} : {}),
|
|
1509
2140
|
reworkFindings: [...findings],
|
|
1510
2141
|
directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
|
|
1511
2142
|
checks: [],
|
|
@@ -1517,13 +2148,81 @@ export class RunIssue {
|
|
|
1517
2148
|
active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
|
|
1518
2149
|
return this.persist(active, {
|
|
1519
2150
|
lifecycle: 'implementing',
|
|
1520
|
-
cycle:
|
|
2151
|
+
cycle: active.record.reviewFeedback?.activeBatch
|
|
2152
|
+
? active.record.cycle
|
|
2153
|
+
: (active.record.cycle + 1),
|
|
2154
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2155
|
+
reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
|
|
2156
|
+
} : {}),
|
|
1521
2157
|
checks: [],
|
|
1522
2158
|
checkedChangeSha256: undefined,
|
|
1523
2159
|
proofId: undefined,
|
|
1524
2160
|
proofReceipt: undefined,
|
|
1525
2161
|
});
|
|
1526
2162
|
}
|
|
2163
|
+
repairBudgetExhausted(active, maxCycles) {
|
|
2164
|
+
return active.record.reviewFeedback?.activeBatch
|
|
2165
|
+
? active.record.reviewFeedback.repairRound >= 3
|
|
2166
|
+
: active.record.cycle >= maxCycles;
|
|
2167
|
+
}
|
|
2168
|
+
async blockReviewFeedback(active, kind, evidenceCode) {
|
|
2169
|
+
const feedback = active.record.reviewFeedback;
|
|
2170
|
+
if (!feedback?.activeBatch)
|
|
2171
|
+
return this.terminal(active, { status: 'blocked', kind, resumable: false }, evidenceCode);
|
|
2172
|
+
{
|
|
2173
|
+
const config = active.config;
|
|
2174
|
+
const blockedLabels = [config.github.labels.auto.name, config.github.labels.blocked.name].sort();
|
|
2175
|
+
const existingIntent = active.record.intent;
|
|
2176
|
+
if (existingIntent?.kind !== 'review-blocked-labels') {
|
|
2177
|
+
active = await this.persist(active, { intent: {
|
|
2178
|
+
kind: 'review-blocked-labels', issueNumber: active.record.issueNumber,
|
|
2179
|
+
batchId: feedback.activeBatch.batchId, expected: blockedLabels, blockKind: kind, evidenceCode,
|
|
2180
|
+
} });
|
|
2181
|
+
}
|
|
2182
|
+
else if (existingIntent.issueNumber !== active.record.issueNumber
|
|
2183
|
+
|| existingIntent.batchId !== feedback.activeBatch.batchId
|
|
2184
|
+
|| !sameStrings(existingIntent.expected, blockedLabels)
|
|
2185
|
+
|| existingIntent.blockKind !== kind
|
|
2186
|
+
|| existingIntent.evidenceCode !== evidenceCode) {
|
|
2187
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-intent-diverged');
|
|
2188
|
+
}
|
|
2189
|
+
let issue = await this.readIssue(active.record.issueNumber);
|
|
2190
|
+
const ownedRunningLabels = [config.github.labels.auto.name, config.github.labels.running.name].sort();
|
|
2191
|
+
const ownedReviewLabels = [config.github.labels.review.name];
|
|
2192
|
+
const canReduceAuthority = !!issue && issue.state === 'OPEN'
|
|
2193
|
+
&& (sameStrings(issue.labels, ownedRunningLabels) || sameStrings(issue.labels, ownedReviewLabels));
|
|
2194
|
+
if (issue?.state === 'OPEN' && !sameStrings(issue.labels, blockedLabels) && !canReduceAuthority) {
|
|
2195
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-source-diverged');
|
|
2196
|
+
}
|
|
2197
|
+
if (canReduceAuthority && !sameStrings(issue.labels, blockedLabels)) {
|
|
2198
|
+
try {
|
|
2199
|
+
await this.dependencies.issues.setLabels(active.record.issueNumber, blockedLabels);
|
|
2200
|
+
}
|
|
2201
|
+
catch {
|
|
2202
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-delivery-unknown');
|
|
2203
|
+
}
|
|
2204
|
+
issue = await this.readIssue(active.record.issueNumber);
|
|
2205
|
+
}
|
|
2206
|
+
if (canReduceAuthority && (!issue || !sameStrings(issue.labels, blockedLabels))) {
|
|
2207
|
+
return this.invokedFailure(active, 'review-feedback-blocked-labels-observation-diverged');
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: `review feedback ${kind}` });
|
|
2211
|
+
const terminalOutcome = { status: 'blocked', kind, resumable: false, evidencePath: evidence.path };
|
|
2212
|
+
const directReview = active.record.directReview && active.record.directReview.status !== 'terminal'
|
|
2213
|
+
? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind })
|
|
2214
|
+
: active.record.directReview;
|
|
2215
|
+
await this.persist(active, {
|
|
2216
|
+
lifecycle: 'blocked',
|
|
2217
|
+
reviewFeedback: blockReviewFeedback(feedback, kind, this.timestamp()),
|
|
2218
|
+
...(directReview ? { directReview } : {}),
|
|
2219
|
+
terminalOutcome,
|
|
2220
|
+
outcomeEvidenceId: evidence.id,
|
|
2221
|
+
process: undefined,
|
|
2222
|
+
intent: undefined,
|
|
2223
|
+
});
|
|
2224
|
+
return publicOutcome(terminalOutcome);
|
|
2225
|
+
}
|
|
1527
2226
|
async mapImplementationFailure(active, result) {
|
|
1528
2227
|
if (result.kind === 'transport-failed')
|
|
1529
2228
|
return this.terminal(active, { status: 'transport-failed', resumable: result.resumable });
|
|
@@ -1545,11 +2244,14 @@ export class RunIssue {
|
|
|
1545
2244
|
async revoked(active) {
|
|
1546
2245
|
return this.terminal(await this.clearIntent(active), { status: 'blocked', kind: 'safety', resumable: true });
|
|
1547
2246
|
}
|
|
1548
|
-
async invokedFailure(active, code) {
|
|
1549
|
-
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary
|
|
2247
|
+
async invokedFailure(active, code, summary = 'Publication delivery requires reconciliation.') {
|
|
2248
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary });
|
|
1550
2249
|
return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
|
|
1551
2250
|
}
|
|
1552
2251
|
async terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
|
|
2252
|
+
if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
|
|
2253
|
+
return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
|
|
2254
|
+
}
|
|
1553
2255
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
|
|
1554
2256
|
const terminalOutcome = { ...outcome, evidencePath: evidence.path };
|
|
1555
2257
|
const changes = {
|
|
@@ -1623,6 +2325,63 @@ class PostEffectStateError extends Error {
|
|
|
1623
2325
|
this.active = active;
|
|
1624
2326
|
}
|
|
1625
2327
|
}
|
|
2328
|
+
const claimMarkerPattern = /^<!-- 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;
|
|
2329
|
+
function isClaimMarkerComment(comment) {
|
|
2330
|
+
return claimMarkerPattern.test(comment.body.split('\n')[0] ?? '');
|
|
2331
|
+
}
|
|
2332
|
+
function claimRunId(comment) {
|
|
2333
|
+
return (comment.body.split('\n')[0] ?? '').match(claimMarkerPattern)?.[1];
|
|
2334
|
+
}
|
|
2335
|
+
function trustedHistoricalClaimBodyKey(comment, issueNumber, branchName) {
|
|
2336
|
+
const runId = claimRunId(comment);
|
|
2337
|
+
if (!runId || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation))
|
|
2338
|
+
return undefined;
|
|
2339
|
+
if (comment.body !== claimComment(runId, issueNumber, branchName))
|
|
2340
|
+
return undefined;
|
|
2341
|
+
return `${comment.authorAssociation}\u0000${comment.body}`;
|
|
2342
|
+
}
|
|
2343
|
+
function frozenHistoricalClaimKey(comment, issueNumber, branchName) {
|
|
2344
|
+
const bodyKey = trustedHistoricalClaimBodyKey(comment, issueNumber, branchName);
|
|
2345
|
+
if (!bodyKey)
|
|
2346
|
+
return undefined;
|
|
2347
|
+
return comment.id ? `id\u0000${comment.id}\u0000${bodyKey}` : `legacy\u0000${bodyKey}`;
|
|
2348
|
+
}
|
|
2349
|
+
function observedHistoricalClaimKeys(comment, record) {
|
|
2350
|
+
const bodyKey = trustedHistoricalClaimBodyKey(comment, record.issueNumber, record.branchName);
|
|
2351
|
+
if (!bodyKey)
|
|
2352
|
+
return [];
|
|
2353
|
+
const keys = comment.id ? [`id\u0000${comment.id}\u0000${bodyKey}`] : [];
|
|
2354
|
+
if (timestampAtOrBefore(comment.createdAt, record.createdAt)
|
|
2355
|
+
&& timestampAtOrBefore(comment.updatedAt, record.createdAt)) {
|
|
2356
|
+
keys.push(`legacy\u0000${bodyKey}`);
|
|
2357
|
+
}
|
|
2358
|
+
return keys;
|
|
2359
|
+
}
|
|
2360
|
+
function timestampAtOrBefore(value, cutoff) {
|
|
2361
|
+
if (!value)
|
|
2362
|
+
return false;
|
|
2363
|
+
const timestamp = Date.parse(value);
|
|
2364
|
+
return !Number.isNaN(timestamp) && timestamp <= Date.parse(cutoff);
|
|
2365
|
+
}
|
|
2366
|
+
function historicalClaimCounts(record) {
|
|
2367
|
+
const counts = new Map();
|
|
2368
|
+
for (const comment of record.issueSnapshot.comments ?? []) {
|
|
2369
|
+
const key = frozenHistoricalClaimKey(comment, record.issueNumber, record.branchName);
|
|
2370
|
+
if (!key || claimRunId(comment) === record.runId)
|
|
2371
|
+
continue;
|
|
2372
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2373
|
+
}
|
|
2374
|
+
return counts;
|
|
2375
|
+
}
|
|
2376
|
+
function refreshClaimedIssueSnapshot(baseline, issue) {
|
|
2377
|
+
return {
|
|
2378
|
+
...structuredClone(baseline),
|
|
2379
|
+
comments: [
|
|
2380
|
+
...(baseline.comments ?? []).filter(isClaimMarkerComment),
|
|
2381
|
+
...issue.comments.filter((comment) => !isClaimMarkerComment(comment)),
|
|
2382
|
+
],
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
1626
2385
|
function snapshotIssue(issue) {
|
|
1627
2386
|
if (issue.state !== 'OPEN')
|
|
1628
2387
|
throw new Error('cannot snapshot a closed issue');
|
|
@@ -1751,12 +2510,30 @@ function sameFreshness(left, right) {
|
|
|
1751
2510
|
&& left.untrackedContentSha256 === right.untrackedContentSha256
|
|
1752
2511
|
&& left.worktreeIdentity === right.worktreeIdentity;
|
|
1753
2512
|
}
|
|
2513
|
+
function expectedImplementationHead(record) {
|
|
2514
|
+
return record.reviewFeedback?.activeBatch?.priorPublishedHeadSha ?? record.baseSha;
|
|
2515
|
+
}
|
|
1754
2516
|
function commentsWithMarker(issue, marker) {
|
|
1755
2517
|
return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
1756
2518
|
}
|
|
1757
2519
|
function sortedUnique(values) {
|
|
1758
2520
|
return [...new Set(values)].sort();
|
|
1759
2521
|
}
|
|
2522
|
+
function worktreeCreationFailureSummary(error) {
|
|
2523
|
+
return boundedLocalGitFailureSummary('Local Git worktree creation failed; correct the local Git state and retry the same run.', error);
|
|
2524
|
+
}
|
|
2525
|
+
function claimedWorktreeInspectionFailureSummary(error) {
|
|
2526
|
+
return boundedLocalGitFailureSummary('The claimed worktree could not be inspected; correct or preserve the local artifact and retry the same run.', error);
|
|
2527
|
+
}
|
|
2528
|
+
function boundedLocalGitFailureSummary(prefix, error) {
|
|
2529
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
2530
|
+
const diagnostic = raw
|
|
2531
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, ' ')
|
|
2532
|
+
.trim();
|
|
2533
|
+
if (diagnostic.length === 0)
|
|
2534
|
+
return prefix;
|
|
2535
|
+
return `${prefix}\n${diagnostic.slice(0, 4 * 1024 - prefix.length - 1)}`;
|
|
2536
|
+
}
|
|
1760
2537
|
function assertUuid(value) {
|
|
1761
2538
|
if (typeof value !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value))
|
|
1762
2539
|
throw new Error('runId is invalid');
|