codex-orchestrator 2.0.6 → 2.0.7
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 +30 -0
- package/README.md +5 -2
- package/dist/src/v2/checked-change.d.ts +1 -1
- package/dist/src/v2/checked-change.d.ts.map +1 -1
- package/dist/src/v2/checked-change.js +3 -2
- package/dist/src/v2/checked-change.js.map +1 -1
- package/dist/src/v2/code-review-report.d.ts +1 -2
- package/dist/src/v2/code-review-report.d.ts.map +1 -1
- package/dist/src/v2/code-review-report.js +6 -11
- package/dist/src/v2/code-review-report.js.map +1 -1
- package/dist/src/v2/containment.d.ts +0 -52
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +1 -195
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +11 -2
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +62 -17
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
- package/dist/src/v2/implementation-reviewer.js +1 -2
- package/dist/src/v2/implementation-reviewer.js.map +1 -1
- package/dist/src/v2/proof-report.d.ts +1 -1
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +6 -4
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +14 -1
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +348 -65
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +14 -1
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +18 -3
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +21 -17
- package/dist/src/v2/runtime.js.map +1 -1
- package/docs/deep-dive.md +16 -10
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/package.json +1 -2
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -3,7 +3,7 @@ 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, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
|
|
6
|
+
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, canRecoverTerminalDirectReviewReport, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, MAX_DIRECT_REVIEW_REPORT_REPAIRS, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, recoverTerminalDirectReviewReport, } from './direct-delivery.js';
|
|
7
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';
|
|
@@ -30,56 +30,88 @@ export class RunIssue {
|
|
|
30
30
|
let observation = await this.readIssue(issueNumber);
|
|
31
31
|
if (!observation || observation.state !== 'OPEN')
|
|
32
32
|
return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
|
|
33
|
+
const body = claimComment(runId, issueNumber, branchName);
|
|
34
|
+
const marker = body.split('\n')[0];
|
|
35
|
+
const commentIntent = { kind: 'comment', issueNumber, marker, bodySha256: sha256(body) };
|
|
36
|
+
// Compatibility with runs persisted by the former label-first claim order.
|
|
37
|
+
// If the label effect is already visible, confirm it. Otherwise establish
|
|
38
|
+
// the trusted claim comment before granting the running status label.
|
|
33
39
|
if (active.record.intent?.kind === 'claim-labels') {
|
|
34
|
-
if (
|
|
40
|
+
if (sameStrings(observation.labels, expectedLabels)) {
|
|
41
|
+
active = await this.confirmEffect(active);
|
|
42
|
+
}
|
|
43
|
+
else if (!this.hasTrustedClaim(observation, active.record)) {
|
|
35
44
|
const labels = new Set(observation.labels);
|
|
36
45
|
if (!labels.has(config.github.labels.auto.name)
|
|
37
46
|
|| labels.has(config.github.labels.blocked.name)
|
|
38
|
-
|| labels.has(config.github.labels.review.name)
|
|
47
|
+
|| labels.has(config.github.labels.review.name)
|
|
48
|
+
|| labels.has(config.github.labels.waitingHuman.name)) {
|
|
39
49
|
return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
|
|
40
50
|
}
|
|
51
|
+
active = await this.persist(active, { intent: commentIntent });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!active.record.intent) {
|
|
55
|
+
observation = await this.readIssue(issueNumber);
|
|
56
|
+
if (!observation || observation.state !== 'OPEN')
|
|
57
|
+
return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
|
|
58
|
+
if (!this.hasTrustedClaim(observation, active.record)) {
|
|
59
|
+
active = await this.persist(active, { intent: commentIntent });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (active.record.intent?.kind === 'comment') {
|
|
63
|
+
if (active.record.intent.marker !== marker || active.record.intent.bodySha256 !== sha256(body)) {
|
|
64
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-intent-diverged') };
|
|
65
|
+
}
|
|
66
|
+
observation = await this.readIssue(issueNumber);
|
|
67
|
+
let comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
68
|
+
if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
|
|
69
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
|
|
70
|
+
}
|
|
71
|
+
if (comments.length === 0) {
|
|
41
72
|
try {
|
|
42
|
-
await this.dependencies.issues.
|
|
73
|
+
await this.dependencies.issues.postComment(issueNumber, body);
|
|
43
74
|
}
|
|
44
75
|
catch {
|
|
45
|
-
return { result: await this.invokedFailure(active, 'claim-
|
|
76
|
+
return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
|
|
46
77
|
}
|
|
47
78
|
observation = await this.readIssue(issueNumber);
|
|
79
|
+
comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
48
80
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
81
|
+
if (comments.length !== 1 || comments[0].body !== body
|
|
82
|
+
|| !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comments[0].authorAssociation)) {
|
|
83
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
|
|
51
84
|
}
|
|
52
85
|
active = await this.confirmEffect(active);
|
|
53
86
|
}
|
|
54
|
-
|
|
55
|
-
|
|
87
|
+
observation = await this.readIssue(issueNumber);
|
|
88
|
+
if (!observation || observation.state !== 'OPEN' || !this.hasTrustedClaim(observation, active.record)) {
|
|
89
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
|
|
90
|
+
}
|
|
91
|
+
if (sameStrings(observation.labels, expectedLabels))
|
|
92
|
+
return { active };
|
|
93
|
+
const labels = new Set(observation.labels);
|
|
94
|
+
if (!labels.has(config.github.labels.auto.name)
|
|
95
|
+
|| labels.has(config.github.labels.blocked.name)
|
|
96
|
+
|| labels.has(config.github.labels.review.name)
|
|
97
|
+
|| labels.has(config.github.labels.waitingHuman.name)) {
|
|
98
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
|
|
56
99
|
}
|
|
57
|
-
const body = claimComment(runId, issueNumber, branchName);
|
|
58
|
-
const marker = body.split('\n')[0];
|
|
59
100
|
if (!active.record.intent) {
|
|
60
|
-
active = await this.persist(active, { intent: { kind: '
|
|
101
|
+
active = await this.persist(active, { intent: { kind: 'claim-labels', issueNumber, expected: expectedLabels } });
|
|
61
102
|
}
|
|
62
|
-
if (active.record.intent?.kind !== '
|
|
63
|
-
return { result: await this.publicationDiverged(active, 'claim-
|
|
103
|
+
if (active.record.intent?.kind !== 'claim-labels' || !sameStrings(active.record.intent.expected, expectedLabels)) {
|
|
104
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-intent-diverged') };
|
|
64
105
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
|
|
68
|
-
return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
|
|
106
|
+
try {
|
|
107
|
+
await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
|
|
69
108
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await this.dependencies.issues.postComment(issueNumber, body);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
|
|
76
|
-
}
|
|
77
|
-
observation = await this.readIssue(issueNumber);
|
|
78
|
-
comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
109
|
+
catch {
|
|
110
|
+
return { result: await this.invokedFailure(active, 'claim-labels-delivery-unknown') };
|
|
79
111
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return { result: await this.publicationDiverged(active, 'claim-
|
|
112
|
+
observation = await this.readIssue(issueNumber);
|
|
113
|
+
if (!observation || !sameStrings(observation.labels, expectedLabels) || !this.hasTrustedClaim(observation, active.record)) {
|
|
114
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-observation-diverged') };
|
|
83
115
|
}
|
|
84
116
|
active = await this.confirmEffect(active);
|
|
85
117
|
return { active };
|
|
@@ -706,17 +738,6 @@ export class RunIssue {
|
|
|
706
738
|
return await this.preClaimInternal('config-changed-during-owner-acquire', input.issueNumber);
|
|
707
739
|
}
|
|
708
740
|
const config = confirmedConfig.config;
|
|
709
|
-
try {
|
|
710
|
-
await this.dependencies.validateContainment(config);
|
|
711
|
-
}
|
|
712
|
-
catch {
|
|
713
|
-
const evidence = await this.dependencies.writeEvidence({
|
|
714
|
-
runId: `issue-${input.issueNumber}`,
|
|
715
|
-
code: 'containment-certificate-invalid',
|
|
716
|
-
summary: 'Containment certification is unavailable or stale.',
|
|
717
|
-
});
|
|
718
|
-
return { status: 'blocked', kind: 'safety', resumable: true, evidencePath: evidence.path };
|
|
719
|
-
}
|
|
720
741
|
if (this.signal.aborted)
|
|
721
742
|
return await this.preClaimCancelled(input.issueNumber);
|
|
722
743
|
let issue;
|
|
@@ -753,15 +774,33 @@ export class RunIssue {
|
|
|
753
774
|
if (active.record.intent?.kind === 'review-blocked-labels') {
|
|
754
775
|
return await this.blockReviewFeedback(active, active.record.intent.blockKind, active.record.intent.evidenceCode);
|
|
755
776
|
}
|
|
777
|
+
if (active.record.intent?.kind === 'blocked-labels') {
|
|
778
|
+
return await this.publishBlockedTerminal(active, {
|
|
779
|
+
status: 'blocked',
|
|
780
|
+
kind: active.record.intent.blockKind,
|
|
781
|
+
resumable: active.record.intent.resumable,
|
|
782
|
+
}, active.record.intent.evidenceCode);
|
|
783
|
+
}
|
|
784
|
+
if (existing.terminalOutcome?.status === 'blocked'
|
|
785
|
+
&& issue?.state === 'OPEN'
|
|
786
|
+
&& blockedLabelProjection(issue.labels, config).status !== 'settled') {
|
|
787
|
+
return await this.reconcilePersistedBlockedTerminal(active, issue, existing.terminalOutcome);
|
|
788
|
+
}
|
|
756
789
|
if (existing.terminalOutcome) {
|
|
757
|
-
if (existing.terminalOutcome.status !== 'review-ready')
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
790
|
+
if (existing.terminalOutcome.status !== 'review-ready') {
|
|
791
|
+
const recovered = await this.recoverTerminalReviewReport(active, issue, config, frozenCriteria);
|
|
792
|
+
if (!recovered)
|
|
793
|
+
return publicOutcome(existing.terminalOutcome);
|
|
794
|
+
active = recovered;
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
const continuation = await this.continueReviewReady(active, targetRoot, config);
|
|
798
|
+
if ('result' in continuation)
|
|
799
|
+
return continuation.result;
|
|
800
|
+
active = continuation.active;
|
|
801
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
802
|
+
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
803
|
+
}
|
|
765
804
|
}
|
|
766
805
|
if (active.record.reviewFeedback?.phase === 'frozen') {
|
|
767
806
|
const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
@@ -896,11 +935,13 @@ export class RunIssue {
|
|
|
896
935
|
worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${input.issueNumber}`);
|
|
897
936
|
baseSha = await this.dependencies.git.getBaseSha({ targetRoot, baseBranch: config.github.baseBranch });
|
|
898
937
|
assertGitSha(baseSha, 'baseSha');
|
|
899
|
-
const
|
|
938
|
+
const claimBody = claimComment(runId, input.issueNumber, branchName);
|
|
900
939
|
active = await this.createRun({
|
|
901
940
|
runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
|
|
902
941
|
issueSnapshot, frozenCriteria, config,
|
|
903
|
-
intent: {
|
|
942
|
+
intent: {
|
|
943
|
+
kind: 'comment', issueNumber: input.issueNumber, marker: claimBody.split('\n')[0], bodySha256: sha256(claimBody),
|
|
944
|
+
},
|
|
904
945
|
});
|
|
905
946
|
const claim = await this.reconcileClaim(active, config);
|
|
906
947
|
if ('result' in claim)
|
|
@@ -927,6 +968,13 @@ export class RunIssue {
|
|
|
927
968
|
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
928
969
|
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
|
|
929
970
|
}
|
|
971
|
+
if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
|
|
972
|
+
&& !active.record.directReview) {
|
|
973
|
+
const baseline = await this.captureBaselineChecks(active, config);
|
|
974
|
+
if ('status' in baseline)
|
|
975
|
+
return baseline;
|
|
976
|
+
active = baseline.active;
|
|
977
|
+
}
|
|
930
978
|
attemptLoop: while (true) {
|
|
931
979
|
if (!await this.authorized(active, config)) {
|
|
932
980
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
@@ -1204,23 +1252,40 @@ export class RunIssue {
|
|
|
1204
1252
|
});
|
|
1205
1253
|
}
|
|
1206
1254
|
}
|
|
1255
|
+
const configuredChecks = Object.entries(config.checks);
|
|
1256
|
+
const reusableChecks = active.record.checks.filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
1257
|
+
if (reusableChecks.length !== active.record.checks.length) {
|
|
1258
|
+
active = await this.persist(active, {
|
|
1259
|
+
checks: reusableChecks,
|
|
1260
|
+
checkedChangeSha256: undefined,
|
|
1261
|
+
proofId: undefined,
|
|
1262
|
+
proofReceipt: undefined,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1207
1265
|
for (const [id, command] of Object.entries(config.checks)) {
|
|
1208
|
-
if (active.record.checks.some((check) => check.id === id
|
|
1266
|
+
if (active.record.checks.some((check) => check.id === id
|
|
1267
|
+
&& check.command === command
|
|
1268
|
+
&& (check.status === 'passed' || check.status === 'unchanged-failure')))
|
|
1209
1269
|
continue;
|
|
1210
1270
|
if (this.signal.aborted)
|
|
1211
1271
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1212
1272
|
let check;
|
|
1213
1273
|
try {
|
|
1214
|
-
check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, signal: this.signal });
|
|
1274
|
+
check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, phase: 'changed', signal: this.signal });
|
|
1215
1275
|
}
|
|
1216
1276
|
catch {
|
|
1217
1277
|
return await this.terminal(active, { status: 'internal-error', code: 'configured-check-execution-failed' });
|
|
1218
1278
|
}
|
|
1219
1279
|
if (this.signal.aborted)
|
|
1220
1280
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1221
|
-
const
|
|
1281
|
+
const outputSha256 = check.outputSha256;
|
|
1282
|
+
const baseline = active.record.baselineChecks?.find((candidate) => candidate.id === id && candidate.command === command);
|
|
1283
|
+
const status = check.status === 'failed' && baseline?.status === 'failed' && baseline.outputSha256 === outputSha256
|
|
1284
|
+
? 'unchanged-failure'
|
|
1285
|
+
: check.status;
|
|
1286
|
+
const row = { id, command, status, outputSha256 };
|
|
1222
1287
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
1223
|
-
if (
|
|
1288
|
+
if (status === 'failed') {
|
|
1224
1289
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1225
1290
|
return active.record.reviewFeedback?.activeBatch
|
|
1226
1291
|
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
@@ -1272,6 +1337,9 @@ export class RunIssue {
|
|
|
1272
1337
|
|| reviewedStageBinding.contentSha256 !== proofContentSha256) {
|
|
1273
1338
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
|
|
1274
1339
|
}
|
|
1340
|
+
if (!sameCheckPolicy(active.record.checks, config.checks)) {
|
|
1341
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
|
|
1342
|
+
}
|
|
1275
1343
|
const payload = {
|
|
1276
1344
|
version: 1,
|
|
1277
1345
|
canonicalRepository,
|
|
@@ -1285,7 +1353,10 @@ export class RunIssue {
|
|
|
1285
1353
|
untrackedContentSha256: freshness.untrackedContentSha256,
|
|
1286
1354
|
worktreeIdentity: freshness.worktreeIdentity,
|
|
1287
1355
|
changedFiles: proofChangedFiles,
|
|
1288
|
-
checks: active.record.checks.map((check) => ({
|
|
1356
|
+
checks: active.record.checks.map((check) => ({
|
|
1357
|
+
...check,
|
|
1358
|
+
status: check.status === 'unchanged-failure' ? 'unchanged-failure' : 'passed',
|
|
1359
|
+
})),
|
|
1289
1360
|
checkPolicySha256: sha256(canonicalJson(config.checks)),
|
|
1290
1361
|
packageVersion: active.record.packageVersion,
|
|
1291
1362
|
proofSchemaVersion: 1,
|
|
@@ -1800,7 +1871,7 @@ export class RunIssue {
|
|
|
1800
1871
|
packageVersion: workflow.receipt.packageVersion,
|
|
1801
1872
|
workflowGeneration: structuredClone(workflow.receipt),
|
|
1802
1873
|
skillHashes: structuredClone(workflow.skillHashes),
|
|
1803
|
-
checks: [], createdAt: now, updatedAt: now,
|
|
1874
|
+
baselineChecks: [], checks: [], createdAt: now, updatedAt: now,
|
|
1804
1875
|
};
|
|
1805
1876
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1806
1877
|
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
@@ -1842,7 +1913,10 @@ export class RunIssue {
|
|
|
1842
1913
|
if (issue.state !== 'OPEN')
|
|
1843
1914
|
return false;
|
|
1844
1915
|
const labels = new Set(issue.labels);
|
|
1845
|
-
if (!labels.has(config.github.labels.auto.name)
|
|
1916
|
+
if (!labels.has(config.github.labels.auto.name)
|
|
1917
|
+
|| labels.has(config.github.labels.blocked.name)
|
|
1918
|
+
|| labels.has(config.github.labels.review.name)
|
|
1919
|
+
|| labels.has(config.github.labels.waitingHuman.name))
|
|
1846
1920
|
return false;
|
|
1847
1921
|
return this.hasTrustedClaim(issue, record);
|
|
1848
1922
|
}
|
|
@@ -1957,9 +2031,7 @@ export class RunIssue {
|
|
|
1957
2031
|
affectedDefectIds: [...directReview.review.affectedDefectIds],
|
|
1958
2032
|
fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
|
|
1959
2033
|
.map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
|
|
1960
|
-
|
|
1961
|
-
? [...directReview.review.coverage]
|
|
1962
|
-
: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
2034
|
+
reviewFocus: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
1963
2035
|
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1964
2036
|
repairOnly: reportRepair !== undefined,
|
|
1965
2037
|
originalReportSha256: reportRepair?.originalReportSha256 ?? null,
|
|
@@ -2029,14 +2101,17 @@ export class RunIssue {
|
|
|
2029
2101
|
const current = active.record.directReview;
|
|
2030
2102
|
if (!current)
|
|
2031
2103
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
2032
|
-
if (current.review.reportRepairs >=
|
|
2033
|
-
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
|
|
2104
|
+
if (current.review.reportRepairs >= MAX_DIRECT_REVIEW_REPORT_REPAIRS) {
|
|
2105
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' }, 'direct-review-report-malformed');
|
|
2034
2106
|
}
|
|
2035
2107
|
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
2036
2108
|
active = await this.persist(active, {
|
|
2037
2109
|
directReview: {
|
|
2038
2110
|
...withoutInvocation,
|
|
2039
|
-
review: {
|
|
2111
|
+
review: {
|
|
2112
|
+
...withoutInvocation.review,
|
|
2113
|
+
reportRepairs: (withoutInvocation.review.reportRepairs + 1),
|
|
2114
|
+
},
|
|
2040
2115
|
},
|
|
2041
2116
|
});
|
|
2042
2117
|
reportRepair = {
|
|
@@ -2111,6 +2186,89 @@ export class RunIssue {
|
|
|
2111
2186
|
}
|
|
2112
2187
|
return this.persist(starting, { directReview: withoutInvocation });
|
|
2113
2188
|
}
|
|
2189
|
+
async captureBaselineChecks(starting, config) {
|
|
2190
|
+
let active = starting;
|
|
2191
|
+
if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0)
|
|
2192
|
+
return { active };
|
|
2193
|
+
const configuredChecks = Object.entries(config.checks);
|
|
2194
|
+
const reusableBaselineChecks = (active.record.baselineChecks ?? []).filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
2195
|
+
if (reusableBaselineChecks.length !== (active.record.baselineChecks ?? []).length) {
|
|
2196
|
+
active = await this.persist(active, { baselineChecks: reusableBaselineChecks });
|
|
2197
|
+
}
|
|
2198
|
+
for (const [id, command] of Object.entries(config.checks)) {
|
|
2199
|
+
if (active.record.baselineChecks?.some((check) => check.id === id && check.command === command))
|
|
2200
|
+
continue;
|
|
2201
|
+
if (this.signal.aborted)
|
|
2202
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
2203
|
+
let check;
|
|
2204
|
+
try {
|
|
2205
|
+
check = await this.dependencies.checks.run({
|
|
2206
|
+
id,
|
|
2207
|
+
command,
|
|
2208
|
+
cwd: active.record.worktreePath,
|
|
2209
|
+
phase: 'baseline',
|
|
2210
|
+
signal: this.signal,
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
catch {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0) {
|
|
2217
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'baseline-check-mutated-worktree');
|
|
2218
|
+
}
|
|
2219
|
+
active = await this.persist(active, {
|
|
2220
|
+
baselineChecks: [
|
|
2221
|
+
...(active.record.baselineChecks ?? []),
|
|
2222
|
+
{ id, command, status: check.status, outputSha256: check.outputSha256 },
|
|
2223
|
+
],
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
return { active };
|
|
2227
|
+
}
|
|
2228
|
+
async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
|
|
2229
|
+
const directReview = active.record.directReview;
|
|
2230
|
+
const allowLegacyMalformed = directReview?.terminalCode === undefined
|
|
2231
|
+
&& active.record.outcomeEvidenceId === `evidence:${active.record.runId}:direct-review-report-malformed`;
|
|
2232
|
+
if (!issue || !directReview || active.record.intent || active.record.process
|
|
2233
|
+
|| active.record.terminalOutcome?.status !== 'internal-error'
|
|
2234
|
+
|| directReview.status !== 'terminal' || directReview.terminalOutcome?.status !== 'internal-error'
|
|
2235
|
+
|| !canRecoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed })
|
|
2236
|
+
|| !this.isAuthorizedIssue(issue, active.record, config))
|
|
2237
|
+
return undefined;
|
|
2238
|
+
let worktree;
|
|
2239
|
+
try {
|
|
2240
|
+
worktree = await this.dependencies.git.inspectWorktree({
|
|
2241
|
+
worktreePath: active.record.worktreePath,
|
|
2242
|
+
branchName: active.record.branchName,
|
|
2243
|
+
baseSha: active.record.baseSha,
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
catch {
|
|
2247
|
+
return undefined;
|
|
2248
|
+
}
|
|
2249
|
+
if (worktree !== 'matching'
|
|
2250
|
+
|| await this.dependencies.git.getHead(active.record.worktreePath) !== expectedImplementationHead(active.record))
|
|
2251
|
+
return undefined;
|
|
2252
|
+
const changedFiles = await this.dependencies.git.listChangedFiles(active.record.worktreePath);
|
|
2253
|
+
if (changedFiles.length === 0 || !active.record.routeReceipt)
|
|
2254
|
+
return undefined;
|
|
2255
|
+
const targetFingerprint = directReviewTargetFingerprint({
|
|
2256
|
+
snapshot: await this.dependencies.git.snapshot(active.record.worktreePath),
|
|
2257
|
+
changedFiles,
|
|
2258
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
2259
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
2260
|
+
cycle: active.record.cycle,
|
|
2261
|
+
frozenCriteria,
|
|
2262
|
+
});
|
|
2263
|
+
if (targetFingerprint !== directReview.targetFingerprint)
|
|
2264
|
+
return undefined;
|
|
2265
|
+
return this.persist(active, {
|
|
2266
|
+
lifecycle: 'implementing',
|
|
2267
|
+
directReview: recoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed }),
|
|
2268
|
+
terminalOutcome: undefined,
|
|
2269
|
+
outcomeEvidenceId: undefined,
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2114
2272
|
async startNextCycle(active, findings, sources) {
|
|
2115
2273
|
if (active.record.directReview?.status === 'clear') {
|
|
2116
2274
|
const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
|
|
@@ -2252,6 +2410,101 @@ export class RunIssue {
|
|
|
2252
2410
|
if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
|
|
2253
2411
|
return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
|
|
2254
2412
|
}
|
|
2413
|
+
if (outcome.status === 'blocked') {
|
|
2414
|
+
return this.publishBlockedTerminal(active, outcome, evidenceCode);
|
|
2415
|
+
}
|
|
2416
|
+
return this.persistTerminal(active, outcome, evidenceCode, retainIntent);
|
|
2417
|
+
}
|
|
2418
|
+
async publishBlockedTerminal(starting, outcome, evidenceCode) {
|
|
2419
|
+
let active = starting;
|
|
2420
|
+
const autoBlocked = [active.config.github.labels.auto.name, active.config.github.labels.blocked.name].sort();
|
|
2421
|
+
const blockedOnly = [active.config.github.labels.blocked.name];
|
|
2422
|
+
let expected = autoBlocked;
|
|
2423
|
+
const existing = active.record.intent;
|
|
2424
|
+
if (!existing) {
|
|
2425
|
+
active = await this.persist(active, { intent: {
|
|
2426
|
+
kind: 'blocked-labels',
|
|
2427
|
+
issueNumber: active.record.issueNumber,
|
|
2428
|
+
expected,
|
|
2429
|
+
blockKind: outcome.kind,
|
|
2430
|
+
resumable: outcome.resumable,
|
|
2431
|
+
evidenceCode,
|
|
2432
|
+
} });
|
|
2433
|
+
}
|
|
2434
|
+
else if (existing.kind !== 'blocked-labels'
|
|
2435
|
+
|| existing.issueNumber !== active.record.issueNumber
|
|
2436
|
+
|| existing.blockKind !== outcome.kind
|
|
2437
|
+
|| existing.resumable !== outcome.resumable
|
|
2438
|
+
|| existing.evidenceCode !== evidenceCode) {
|
|
2439
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2440
|
+
}
|
|
2441
|
+
else {
|
|
2442
|
+
expected = existing.expected;
|
|
2443
|
+
if (!sameStrings(expected, autoBlocked) && !sameStrings(expected, blockedOnly) && expected.length !== 0) {
|
|
2444
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
let issue = await this.readIssue(active.record.issueNumber);
|
|
2448
|
+
if (!issue)
|
|
2449
|
+
return this.invokedFailure(active, 'blocked-labels-issue-missing');
|
|
2450
|
+
let projection = blockedLabelProjection(issue.labels, active.config);
|
|
2451
|
+
if (issue.state === 'OPEN' && projection.status === 'transition') {
|
|
2452
|
+
if (!this.dependencies.issues.transitionToBlocked) {
|
|
2453
|
+
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2454
|
+
}
|
|
2455
|
+
try {
|
|
2456
|
+
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2457
|
+
}
|
|
2458
|
+
catch {
|
|
2459
|
+
return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
|
|
2460
|
+
}
|
|
2461
|
+
issue = await this.readIssue(active.record.issueNumber);
|
|
2462
|
+
projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
|
|
2463
|
+
}
|
|
2464
|
+
if (!issue)
|
|
2465
|
+
return this.invokedFailure(active, 'blocked-labels-observation-missing');
|
|
2466
|
+
if (issue.state === 'OPEN') {
|
|
2467
|
+
if (projection.status !== 'settled') {
|
|
2468
|
+
return this.invokedFailure(active, 'blocked-labels-observation-diverged');
|
|
2469
|
+
}
|
|
2470
|
+
if (!sameStrings(expected, projection.expected)) {
|
|
2471
|
+
expected = projection.expected;
|
|
2472
|
+
const intent = active.record.intent;
|
|
2473
|
+
if (intent?.kind !== 'blocked-labels')
|
|
2474
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2475
|
+
try {
|
|
2476
|
+
active = await this.persist(active, { intent: { ...intent, expected } });
|
|
2477
|
+
}
|
|
2478
|
+
catch {
|
|
2479
|
+
throw new PostEffectStateError(active);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
try {
|
|
2484
|
+
return await this.persistTerminal(active, outcome, evidenceCode, false);
|
|
2485
|
+
}
|
|
2486
|
+
catch {
|
|
2487
|
+
throw new PostEffectStateError(active);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
async reconcilePersistedBlockedTerminal(active, issue, outcome) {
|
|
2491
|
+
if (!this.dependencies.issues.transitionToBlocked) {
|
|
2492
|
+
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2493
|
+
}
|
|
2494
|
+
try {
|
|
2495
|
+
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2496
|
+
}
|
|
2497
|
+
catch {
|
|
2498
|
+
return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
|
|
2499
|
+
}
|
|
2500
|
+
const observation = await this.readIssue(active.record.issueNumber);
|
|
2501
|
+
if (!observation || observation.state !== 'OPEN'
|
|
2502
|
+
|| blockedLabelProjection(observation.labels, active.config).status !== 'settled') {
|
|
2503
|
+
return this.invokedFailure(active, 'blocked-labels-observation-diverged');
|
|
2504
|
+
}
|
|
2505
|
+
return publicOutcome(outcome);
|
|
2506
|
+
}
|
|
2507
|
+
async persistTerminal(active, outcome, evidenceCode, retainIntent) {
|
|
2255
2508
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
|
|
2256
2509
|
const terminalOutcome = { ...outcome, evidencePath: evidence.path };
|
|
2257
2510
|
const changes = {
|
|
@@ -2263,7 +2516,7 @@ export class RunIssue {
|
|
|
2263
2516
|
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
2264
2517
|
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
2265
2518
|
? { status: 'blocked', kind: outcome.kind }
|
|
2266
|
-
: { status: outcome.status });
|
|
2519
|
+
: { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
|
|
2267
2520
|
}
|
|
2268
2521
|
if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
|
|
2269
2522
|
if (active.record.waitingHuman.phase === 'resumed') {
|
|
@@ -2503,6 +2756,36 @@ function sameStrings(left, right) {
|
|
|
2503
2756
|
const b = [...right].sort();
|
|
2504
2757
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
2505
2758
|
}
|
|
2759
|
+
function blockedLabelPolicy(config) {
|
|
2760
|
+
return {
|
|
2761
|
+
auto: config.github.labels.auto.name,
|
|
2762
|
+
running: config.github.labels.running.name,
|
|
2763
|
+
blocked: config.github.labels.blocked.name,
|
|
2764
|
+
review: config.github.labels.review.name,
|
|
2765
|
+
waitingHuman: config.github.labels.waitingHuman.name,
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
function blockedLabelProjection(labels, config) {
|
|
2769
|
+
const policy = blockedLabelPolicy(config);
|
|
2770
|
+
const present = new Set(labels);
|
|
2771
|
+
if (present.has(policy.review) || present.has(policy.waitingHuman))
|
|
2772
|
+
return { status: 'diverged' };
|
|
2773
|
+
const auto = present.has(policy.auto);
|
|
2774
|
+
const running = present.has(policy.running);
|
|
2775
|
+
const blocked = present.has(policy.blocked);
|
|
2776
|
+
if (running || (auto && !blocked))
|
|
2777
|
+
return { status: 'transition' };
|
|
2778
|
+
if (blocked)
|
|
2779
|
+
return { status: 'settled', expected: auto ? [policy.auto, policy.blocked].sort() : [policy.blocked] };
|
|
2780
|
+
return { status: 'settled', expected: [] };
|
|
2781
|
+
}
|
|
2782
|
+
function sameCheckPolicy(checks, policy) {
|
|
2783
|
+
const expected = Object.entries(policy).sort(([left], [right]) => left.localeCompare(right));
|
|
2784
|
+
const actual = checks.map((check) => [check.id, check.command])
|
|
2785
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
2786
|
+
return actual.length === expected.length
|
|
2787
|
+
&& actual.every(([id, command], index) => id === expected[index]?.[0] && command === expected[index]?.[1]);
|
|
2788
|
+
}
|
|
2506
2789
|
function sameFreshness(left, right) {
|
|
2507
2790
|
return left.headSha === right.headSha
|
|
2508
2791
|
&& left.indexTreeSha === right.indexTreeSha
|