codex-orchestrator 2.0.6 → 2.0.8
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 +42 -0
- package/README.md +8 -4
- 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/issue-check-policy.d.ts +17 -0
- package/dist/src/v2/issue-check-policy.d.ts.map +1 -0
- package/dist/src/v2/issue-check-policy.js +133 -0
- package/dist/src/v2/issue-check-policy.js.map +1 -0
- 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 +15 -1
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +374 -69
- 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 +5 -0
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +86 -23
- package/dist/src/v2/runtime.js.map +1 -1
- package/docs/deep-dive.md +21 -11
- 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,8 +3,9 @@ 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
|
+
import { CheckProcessQuiescenceError, resolveIssueCheckPolicy } from './issue-check-policy.js';
|
|
8
9
|
import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
|
|
9
10
|
import { initialRouteExecution, } from './route-coordinator.js';
|
|
10
11
|
import { downstreamLifecycleForRoute, validateRouteTransition, validateTrustedAnswerResumeTransition, } from './route-decision.js';
|
|
@@ -30,56 +31,88 @@ export class RunIssue {
|
|
|
30
31
|
let observation = await this.readIssue(issueNumber);
|
|
31
32
|
if (!observation || observation.state !== 'OPEN')
|
|
32
33
|
return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
|
|
34
|
+
const body = claimComment(runId, issueNumber, branchName);
|
|
35
|
+
const marker = body.split('\n')[0];
|
|
36
|
+
const commentIntent = { kind: 'comment', issueNumber, marker, bodySha256: sha256(body) };
|
|
37
|
+
// Compatibility with runs persisted by the former label-first claim order.
|
|
38
|
+
// If the label effect is already visible, confirm it. Otherwise establish
|
|
39
|
+
// the trusted claim comment before granting the running status label.
|
|
33
40
|
if (active.record.intent?.kind === 'claim-labels') {
|
|
34
|
-
if (
|
|
41
|
+
if (sameStrings(observation.labels, expectedLabels)) {
|
|
42
|
+
active = await this.confirmEffect(active);
|
|
43
|
+
}
|
|
44
|
+
else if (!this.hasTrustedClaim(observation, active.record)) {
|
|
35
45
|
const labels = new Set(observation.labels);
|
|
36
46
|
if (!labels.has(config.github.labels.auto.name)
|
|
37
47
|
|| labels.has(config.github.labels.blocked.name)
|
|
38
|
-
|| labels.has(config.github.labels.review.name)
|
|
48
|
+
|| labels.has(config.github.labels.review.name)
|
|
49
|
+
|| labels.has(config.github.labels.waitingHuman.name)) {
|
|
39
50
|
return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
|
|
40
51
|
}
|
|
52
|
+
active = await this.persist(active, { intent: commentIntent });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!active.record.intent) {
|
|
56
|
+
observation = await this.readIssue(issueNumber);
|
|
57
|
+
if (!observation || observation.state !== 'OPEN')
|
|
58
|
+
return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
|
|
59
|
+
if (!this.hasTrustedClaim(observation, active.record)) {
|
|
60
|
+
active = await this.persist(active, { intent: commentIntent });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (active.record.intent?.kind === 'comment') {
|
|
64
|
+
if (active.record.intent.marker !== marker || active.record.intent.bodySha256 !== sha256(body)) {
|
|
65
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-intent-diverged') };
|
|
66
|
+
}
|
|
67
|
+
observation = await this.readIssue(issueNumber);
|
|
68
|
+
let comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
69
|
+
if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
|
|
70
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
|
|
71
|
+
}
|
|
72
|
+
if (comments.length === 0) {
|
|
41
73
|
try {
|
|
42
|
-
await this.dependencies.issues.
|
|
74
|
+
await this.dependencies.issues.postComment(issueNumber, body);
|
|
43
75
|
}
|
|
44
76
|
catch {
|
|
45
|
-
return { result: await this.invokedFailure(active, 'claim-
|
|
77
|
+
return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
|
|
46
78
|
}
|
|
47
79
|
observation = await this.readIssue(issueNumber);
|
|
80
|
+
comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
48
81
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
82
|
+
if (comments.length !== 1 || comments[0].body !== body
|
|
83
|
+
|| !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comments[0].authorAssociation)) {
|
|
84
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
|
|
51
85
|
}
|
|
52
86
|
active = await this.confirmEffect(active);
|
|
53
87
|
}
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
observation = await this.readIssue(issueNumber);
|
|
89
|
+
if (!observation || observation.state !== 'OPEN' || !this.hasTrustedClaim(observation, active.record)) {
|
|
90
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
|
|
91
|
+
}
|
|
92
|
+
if (sameStrings(observation.labels, expectedLabels))
|
|
93
|
+
return { active };
|
|
94
|
+
const labels = new Set(observation.labels);
|
|
95
|
+
if (!labels.has(config.github.labels.auto.name)
|
|
96
|
+
|| labels.has(config.github.labels.blocked.name)
|
|
97
|
+
|| labels.has(config.github.labels.review.name)
|
|
98
|
+
|| labels.has(config.github.labels.waitingHuman.name)) {
|
|
99
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
|
|
56
100
|
}
|
|
57
|
-
const body = claimComment(runId, issueNumber, branchName);
|
|
58
|
-
const marker = body.split('\n')[0];
|
|
59
101
|
if (!active.record.intent) {
|
|
60
|
-
active = await this.persist(active, { intent: { kind: '
|
|
102
|
+
active = await this.persist(active, { intent: { kind: 'claim-labels', issueNumber, expected: expectedLabels } });
|
|
61
103
|
}
|
|
62
|
-
if (active.record.intent?.kind !== '
|
|
63
|
-
return { result: await this.publicationDiverged(active, 'claim-
|
|
104
|
+
if (active.record.intent?.kind !== 'claim-labels' || !sameStrings(active.record.intent.expected, expectedLabels)) {
|
|
105
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-intent-diverged') };
|
|
64
106
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
|
|
68
|
-
return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
|
|
107
|
+
try {
|
|
108
|
+
await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
|
|
69
109
|
}
|
|
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) : [];
|
|
110
|
+
catch {
|
|
111
|
+
return { result: await this.invokedFailure(active, 'claim-labels-delivery-unknown') };
|
|
79
112
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return { result: await this.publicationDiverged(active, 'claim-
|
|
113
|
+
observation = await this.readIssue(issueNumber);
|
|
114
|
+
if (!observation || !sameStrings(observation.labels, expectedLabels) || !this.hasTrustedClaim(observation, active.record)) {
|
|
115
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-observation-diverged') };
|
|
83
116
|
}
|
|
84
117
|
active = await this.confirmEffect(active);
|
|
85
118
|
return { active };
|
|
@@ -706,17 +739,6 @@ export class RunIssue {
|
|
|
706
739
|
return await this.preClaimInternal('config-changed-during-owner-acquire', input.issueNumber);
|
|
707
740
|
}
|
|
708
741
|
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
742
|
if (this.signal.aborted)
|
|
721
743
|
return await this.preClaimCancelled(input.issueNumber);
|
|
722
744
|
let issue;
|
|
@@ -753,15 +775,33 @@ export class RunIssue {
|
|
|
753
775
|
if (active.record.intent?.kind === 'review-blocked-labels') {
|
|
754
776
|
return await this.blockReviewFeedback(active, active.record.intent.blockKind, active.record.intent.evidenceCode);
|
|
755
777
|
}
|
|
778
|
+
if (active.record.intent?.kind === 'blocked-labels') {
|
|
779
|
+
return await this.publishBlockedTerminal(active, {
|
|
780
|
+
status: 'blocked',
|
|
781
|
+
kind: active.record.intent.blockKind,
|
|
782
|
+
resumable: active.record.intent.resumable,
|
|
783
|
+
}, active.record.intent.evidenceCode);
|
|
784
|
+
}
|
|
785
|
+
if (existing.terminalOutcome?.status === 'blocked'
|
|
786
|
+
&& issue?.state === 'OPEN'
|
|
787
|
+
&& blockedLabelProjection(issue.labels, config).status !== 'settled') {
|
|
788
|
+
return await this.reconcilePersistedBlockedTerminal(active, issue, existing.terminalOutcome);
|
|
789
|
+
}
|
|
756
790
|
if (existing.terminalOutcome) {
|
|
757
|
-
if (existing.terminalOutcome.status !== 'review-ready')
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
791
|
+
if (existing.terminalOutcome.status !== 'review-ready') {
|
|
792
|
+
const recovered = await this.recoverTerminalReviewReport(active, issue, config, frozenCriteria);
|
|
793
|
+
if (!recovered)
|
|
794
|
+
return publicOutcome(existing.terminalOutcome);
|
|
795
|
+
active = recovered;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
const continuation = await this.continueReviewReady(active, targetRoot, config);
|
|
799
|
+
if ('result' in continuation)
|
|
800
|
+
return continuation.result;
|
|
801
|
+
active = continuation.active;
|
|
802
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
803
|
+
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
804
|
+
}
|
|
765
805
|
}
|
|
766
806
|
if (active.record.reviewFeedback?.phase === 'frozen') {
|
|
767
807
|
const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
@@ -896,11 +936,13 @@ export class RunIssue {
|
|
|
896
936
|
worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${input.issueNumber}`);
|
|
897
937
|
baseSha = await this.dependencies.git.getBaseSha({ targetRoot, baseBranch: config.github.baseBranch });
|
|
898
938
|
assertGitSha(baseSha, 'baseSha');
|
|
899
|
-
const
|
|
939
|
+
const claimBody = claimComment(runId, input.issueNumber, branchName);
|
|
900
940
|
active = await this.createRun({
|
|
901
941
|
runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
|
|
902
942
|
issueSnapshot, frozenCriteria, config,
|
|
903
|
-
intent: {
|
|
943
|
+
intent: {
|
|
944
|
+
kind: 'comment', issueNumber: input.issueNumber, marker: claimBody.split('\n')[0], bodySha256: sha256(claimBody),
|
|
945
|
+
},
|
|
904
946
|
});
|
|
905
947
|
const claim = await this.reconcileClaim(active, config);
|
|
906
948
|
if ('result' in claim)
|
|
@@ -927,6 +969,13 @@ export class RunIssue {
|
|
|
927
969
|
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
928
970
|
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
|
|
929
971
|
}
|
|
972
|
+
if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
|
|
973
|
+
&& !active.record.directReview) {
|
|
974
|
+
const baseline = await this.captureBaselineChecks(active, config);
|
|
975
|
+
if ('status' in baseline)
|
|
976
|
+
return baseline;
|
|
977
|
+
active = baseline.active;
|
|
978
|
+
}
|
|
930
979
|
attemptLoop: while (true) {
|
|
931
980
|
if (!await this.authorized(active, config)) {
|
|
932
981
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
@@ -1204,23 +1253,50 @@ export class RunIssue {
|
|
|
1204
1253
|
});
|
|
1205
1254
|
}
|
|
1206
1255
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1256
|
+
let checkPolicy;
|
|
1257
|
+
try {
|
|
1258
|
+
checkPolicy = resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks);
|
|
1259
|
+
}
|
|
1260
|
+
catch (error) {
|
|
1261
|
+
return await this.invokedFailure(active, 'issue-verification-invalid', error instanceof Error ? error.message : undefined);
|
|
1262
|
+
}
|
|
1263
|
+
const configuredChecks = Object.entries(checkPolicy.checks);
|
|
1264
|
+
const reusableChecks = active.record.checks.filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
1265
|
+
if (reusableChecks.length !== active.record.checks.length) {
|
|
1266
|
+
active = await this.persist(active, {
|
|
1267
|
+
checks: reusableChecks,
|
|
1268
|
+
checkedChangeSha256: undefined,
|
|
1269
|
+
proofId: undefined,
|
|
1270
|
+
proofReceipt: undefined,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
for (const [id, command] of configuredChecks) {
|
|
1274
|
+
if (active.record.checks.some((check) => check.id === id
|
|
1275
|
+
&& check.command === command
|
|
1276
|
+
&& (check.status === 'passed' || check.status === 'unchanged-failure')))
|
|
1209
1277
|
continue;
|
|
1210
1278
|
if (this.signal.aborted)
|
|
1211
1279
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1212
1280
|
let check;
|
|
1213
1281
|
try {
|
|
1214
|
-
check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, signal: this.signal });
|
|
1282
|
+
check = await this.dependencies.checks.run({ id, command, source: checkPolicy.source, cwd: worktreePath, phase: 'changed', signal: this.signal });
|
|
1215
1283
|
}
|
|
1216
|
-
catch {
|
|
1217
|
-
|
|
1284
|
+
catch (error) {
|
|
1285
|
+
if (error instanceof CheckProcessQuiescenceError) {
|
|
1286
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'check-process-quiescence-unconfirmed');
|
|
1287
|
+
}
|
|
1288
|
+
return await this.invokedFailure(active, 'configured-check-execution-failed', error instanceof Error ? error.message : 'The check process did not start or settle. Retry the same run.');
|
|
1218
1289
|
}
|
|
1219
1290
|
if (this.signal.aborted)
|
|
1220
1291
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1221
|
-
const
|
|
1292
|
+
const outputSha256 = check.outputSha256;
|
|
1293
|
+
const baseline = active.record.baselineChecks?.find((candidate) => candidate.id === id && candidate.command === command);
|
|
1294
|
+
const status = check.status === 'failed' && baseline?.status === 'failed' && baseline.outputSha256 === outputSha256
|
|
1295
|
+
? 'unchanged-failure'
|
|
1296
|
+
: check.status;
|
|
1297
|
+
const row = { id, command, status, outputSha256 };
|
|
1222
1298
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
1223
|
-
if (
|
|
1299
|
+
if (status === 'failed') {
|
|
1224
1300
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1225
1301
|
return active.record.reviewFeedback?.activeBatch
|
|
1226
1302
|
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
@@ -1272,6 +1348,9 @@ export class RunIssue {
|
|
|
1272
1348
|
|| reviewedStageBinding.contentSha256 !== proofContentSha256) {
|
|
1273
1349
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
|
|
1274
1350
|
}
|
|
1351
|
+
if (!sameCheckPolicy(active.record.checks, checkPolicy.checks)) {
|
|
1352
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
|
|
1353
|
+
}
|
|
1275
1354
|
const payload = {
|
|
1276
1355
|
version: 1,
|
|
1277
1356
|
canonicalRepository,
|
|
@@ -1285,8 +1364,11 @@ export class RunIssue {
|
|
|
1285
1364
|
untrackedContentSha256: freshness.untrackedContentSha256,
|
|
1286
1365
|
worktreeIdentity: freshness.worktreeIdentity,
|
|
1287
1366
|
changedFiles: proofChangedFiles,
|
|
1288
|
-
checks: active.record.checks.map((check) => ({
|
|
1289
|
-
|
|
1367
|
+
checks: active.record.checks.map((check) => ({
|
|
1368
|
+
...check,
|
|
1369
|
+
status: check.status === 'unchanged-failure' ? 'unchanged-failure' : 'passed',
|
|
1370
|
+
})),
|
|
1371
|
+
checkPolicySha256: sha256(canonicalJson(checkPolicy.checks)),
|
|
1290
1372
|
packageVersion: active.record.packageVersion,
|
|
1291
1373
|
proofSchemaVersion: 1,
|
|
1292
1374
|
};
|
|
@@ -1800,7 +1882,7 @@ export class RunIssue {
|
|
|
1800
1882
|
packageVersion: workflow.receipt.packageVersion,
|
|
1801
1883
|
workflowGeneration: structuredClone(workflow.receipt),
|
|
1802
1884
|
skillHashes: structuredClone(workflow.skillHashes),
|
|
1803
|
-
checks: [], createdAt: now, updatedAt: now,
|
|
1885
|
+
baselineChecks: [], checks: [], createdAt: now, updatedAt: now,
|
|
1804
1886
|
};
|
|
1805
1887
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1806
1888
|
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
@@ -1842,7 +1924,10 @@ export class RunIssue {
|
|
|
1842
1924
|
if (issue.state !== 'OPEN')
|
|
1843
1925
|
return false;
|
|
1844
1926
|
const labels = new Set(issue.labels);
|
|
1845
|
-
if (!labels.has(config.github.labels.auto.name)
|
|
1927
|
+
if (!labels.has(config.github.labels.auto.name)
|
|
1928
|
+
|| labels.has(config.github.labels.blocked.name)
|
|
1929
|
+
|| labels.has(config.github.labels.review.name)
|
|
1930
|
+
|| labels.has(config.github.labels.waitingHuman.name))
|
|
1846
1931
|
return false;
|
|
1847
1932
|
return this.hasTrustedClaim(issue, record);
|
|
1848
1933
|
}
|
|
@@ -1957,9 +2042,7 @@ export class RunIssue {
|
|
|
1957
2042
|
affectedDefectIds: [...directReview.review.affectedDefectIds],
|
|
1958
2043
|
fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
|
|
1959
2044
|
.map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
|
|
1960
|
-
|
|
1961
|
-
? [...directReview.review.coverage]
|
|
1962
|
-
: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
2045
|
+
reviewFocus: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
1963
2046
|
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1964
2047
|
repairOnly: reportRepair !== undefined,
|
|
1965
2048
|
originalReportSha256: reportRepair?.originalReportSha256 ?? null,
|
|
@@ -2029,14 +2112,17 @@ export class RunIssue {
|
|
|
2029
2112
|
const current = active.record.directReview;
|
|
2030
2113
|
if (!current)
|
|
2031
2114
|
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' });
|
|
2115
|
+
if (current.review.reportRepairs >= MAX_DIRECT_REVIEW_REPORT_REPAIRS) {
|
|
2116
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' }, 'direct-review-report-malformed');
|
|
2034
2117
|
}
|
|
2035
2118
|
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
2036
2119
|
active = await this.persist(active, {
|
|
2037
2120
|
directReview: {
|
|
2038
2121
|
...withoutInvocation,
|
|
2039
|
-
review: {
|
|
2122
|
+
review: {
|
|
2123
|
+
...withoutInvocation.review,
|
|
2124
|
+
reportRepairs: (withoutInvocation.review.reportRepairs + 1),
|
|
2125
|
+
},
|
|
2040
2126
|
},
|
|
2041
2127
|
});
|
|
2042
2128
|
reportRepair = {
|
|
@@ -2111,6 +2197,100 @@ export class RunIssue {
|
|
|
2111
2197
|
}
|
|
2112
2198
|
return this.persist(starting, { directReview: withoutInvocation });
|
|
2113
2199
|
}
|
|
2200
|
+
async captureBaselineChecks(starting, config) {
|
|
2201
|
+
let active = starting;
|
|
2202
|
+
if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0)
|
|
2203
|
+
return { active };
|
|
2204
|
+
let checkPolicy;
|
|
2205
|
+
try {
|
|
2206
|
+
checkPolicy = resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks);
|
|
2207
|
+
}
|
|
2208
|
+
catch (error) {
|
|
2209
|
+
active = await this.persist(active, { lifecycle: 'routed' });
|
|
2210
|
+
return this.invokedFailure(active, 'issue-verification-invalid', error instanceof Error ? error.message : undefined);
|
|
2211
|
+
}
|
|
2212
|
+
const configuredChecks = Object.entries(checkPolicy.checks);
|
|
2213
|
+
const reusableBaselineChecks = (active.record.baselineChecks ?? []).filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
2214
|
+
if (reusableBaselineChecks.length !== (active.record.baselineChecks ?? []).length) {
|
|
2215
|
+
active = await this.persist(active, { baselineChecks: reusableBaselineChecks });
|
|
2216
|
+
}
|
|
2217
|
+
for (const [id, command] of configuredChecks) {
|
|
2218
|
+
if (active.record.baselineChecks?.some((check) => check.id === id && check.command === command))
|
|
2219
|
+
continue;
|
|
2220
|
+
if (this.signal.aborted)
|
|
2221
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
2222
|
+
let check;
|
|
2223
|
+
try {
|
|
2224
|
+
check = await this.dependencies.checks.run({
|
|
2225
|
+
id, command, source: checkPolicy.source,
|
|
2226
|
+
cwd: active.record.worktreePath,
|
|
2227
|
+
phase: 'baseline',
|
|
2228
|
+
signal: this.signal,
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
catch (error) {
|
|
2232
|
+
if (error instanceof CheckProcessQuiescenceError) {
|
|
2233
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'check-process-quiescence-unconfirmed');
|
|
2234
|
+
}
|
|
2235
|
+
active = await this.persist(active, { lifecycle: 'routed' });
|
|
2236
|
+
return this.invokedFailure(active, 'configured-check-execution-failed', error instanceof Error ? error.message : 'The baseline check process did not start or settle. Retry the same run.');
|
|
2237
|
+
}
|
|
2238
|
+
if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0) {
|
|
2239
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'baseline-check-mutated-worktree');
|
|
2240
|
+
}
|
|
2241
|
+
active = await this.persist(active, {
|
|
2242
|
+
baselineChecks: [
|
|
2243
|
+
...(active.record.baselineChecks ?? []),
|
|
2244
|
+
{ id, command, status: check.status, outputSha256: check.outputSha256 },
|
|
2245
|
+
],
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
return { active };
|
|
2249
|
+
}
|
|
2250
|
+
async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
|
|
2251
|
+
const directReview = active.record.directReview;
|
|
2252
|
+
const allowLegacyMalformed = directReview?.terminalCode === undefined
|
|
2253
|
+
&& active.record.outcomeEvidenceId === `evidence:${active.record.runId}:direct-review-report-malformed`;
|
|
2254
|
+
if (!issue || !directReview || active.record.intent || active.record.process
|
|
2255
|
+
|| active.record.terminalOutcome?.status !== 'internal-error'
|
|
2256
|
+
|| directReview.status !== 'terminal' || directReview.terminalOutcome?.status !== 'internal-error'
|
|
2257
|
+
|| !canRecoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed })
|
|
2258
|
+
|| !this.isAuthorizedIssue(issue, active.record, config))
|
|
2259
|
+
return undefined;
|
|
2260
|
+
let worktree;
|
|
2261
|
+
try {
|
|
2262
|
+
worktree = await this.dependencies.git.inspectWorktree({
|
|
2263
|
+
worktreePath: active.record.worktreePath,
|
|
2264
|
+
branchName: active.record.branchName,
|
|
2265
|
+
baseSha: active.record.baseSha,
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
catch {
|
|
2269
|
+
return undefined;
|
|
2270
|
+
}
|
|
2271
|
+
if (worktree !== 'matching'
|
|
2272
|
+
|| await this.dependencies.git.getHead(active.record.worktreePath) !== expectedImplementationHead(active.record))
|
|
2273
|
+
return undefined;
|
|
2274
|
+
const changedFiles = await this.dependencies.git.listChangedFiles(active.record.worktreePath);
|
|
2275
|
+
if (changedFiles.length === 0 || !active.record.routeReceipt)
|
|
2276
|
+
return undefined;
|
|
2277
|
+
const targetFingerprint = directReviewTargetFingerprint({
|
|
2278
|
+
snapshot: await this.dependencies.git.snapshot(active.record.worktreePath),
|
|
2279
|
+
changedFiles,
|
|
2280
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
2281
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
2282
|
+
cycle: active.record.cycle,
|
|
2283
|
+
frozenCriteria,
|
|
2284
|
+
});
|
|
2285
|
+
if (targetFingerprint !== directReview.targetFingerprint)
|
|
2286
|
+
return undefined;
|
|
2287
|
+
return this.persist(active, {
|
|
2288
|
+
lifecycle: 'implementing',
|
|
2289
|
+
directReview: recoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed }),
|
|
2290
|
+
terminalOutcome: undefined,
|
|
2291
|
+
outcomeEvidenceId: undefined,
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2114
2294
|
async startNextCycle(active, findings, sources) {
|
|
2115
2295
|
if (active.record.directReview?.status === 'clear') {
|
|
2116
2296
|
const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
|
|
@@ -2252,6 +2432,101 @@ export class RunIssue {
|
|
|
2252
2432
|
if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
|
|
2253
2433
|
return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
|
|
2254
2434
|
}
|
|
2435
|
+
if (outcome.status === 'blocked') {
|
|
2436
|
+
return this.publishBlockedTerminal(active, outcome, evidenceCode);
|
|
2437
|
+
}
|
|
2438
|
+
return this.persistTerminal(active, outcome, evidenceCode, retainIntent);
|
|
2439
|
+
}
|
|
2440
|
+
async publishBlockedTerminal(starting, outcome, evidenceCode) {
|
|
2441
|
+
let active = starting;
|
|
2442
|
+
const autoBlocked = [active.config.github.labels.auto.name, active.config.github.labels.blocked.name].sort();
|
|
2443
|
+
const blockedOnly = [active.config.github.labels.blocked.name];
|
|
2444
|
+
let expected = autoBlocked;
|
|
2445
|
+
const existing = active.record.intent;
|
|
2446
|
+
if (!existing) {
|
|
2447
|
+
active = await this.persist(active, { intent: {
|
|
2448
|
+
kind: 'blocked-labels',
|
|
2449
|
+
issueNumber: active.record.issueNumber,
|
|
2450
|
+
expected,
|
|
2451
|
+
blockKind: outcome.kind,
|
|
2452
|
+
resumable: outcome.resumable,
|
|
2453
|
+
evidenceCode,
|
|
2454
|
+
} });
|
|
2455
|
+
}
|
|
2456
|
+
else if (existing.kind !== 'blocked-labels'
|
|
2457
|
+
|| existing.issueNumber !== active.record.issueNumber
|
|
2458
|
+
|| existing.blockKind !== outcome.kind
|
|
2459
|
+
|| existing.resumable !== outcome.resumable
|
|
2460
|
+
|| existing.evidenceCode !== evidenceCode) {
|
|
2461
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2462
|
+
}
|
|
2463
|
+
else {
|
|
2464
|
+
expected = existing.expected;
|
|
2465
|
+
if (!sameStrings(expected, autoBlocked) && !sameStrings(expected, blockedOnly) && expected.length !== 0) {
|
|
2466
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
let issue = await this.readIssue(active.record.issueNumber);
|
|
2470
|
+
if (!issue)
|
|
2471
|
+
return this.invokedFailure(active, 'blocked-labels-issue-missing');
|
|
2472
|
+
let projection = blockedLabelProjection(issue.labels, active.config);
|
|
2473
|
+
if (issue.state === 'OPEN' && projection.status === 'transition') {
|
|
2474
|
+
if (!this.dependencies.issues.transitionToBlocked) {
|
|
2475
|
+
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2476
|
+
}
|
|
2477
|
+
try {
|
|
2478
|
+
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2479
|
+
}
|
|
2480
|
+
catch {
|
|
2481
|
+
return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
|
|
2482
|
+
}
|
|
2483
|
+
issue = await this.readIssue(active.record.issueNumber);
|
|
2484
|
+
projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
|
|
2485
|
+
}
|
|
2486
|
+
if (!issue)
|
|
2487
|
+
return this.invokedFailure(active, 'blocked-labels-observation-missing');
|
|
2488
|
+
if (issue.state === 'OPEN') {
|
|
2489
|
+
if (projection.status !== 'settled') {
|
|
2490
|
+
return this.invokedFailure(active, 'blocked-labels-observation-diverged');
|
|
2491
|
+
}
|
|
2492
|
+
if (!sameStrings(expected, projection.expected)) {
|
|
2493
|
+
expected = projection.expected;
|
|
2494
|
+
const intent = active.record.intent;
|
|
2495
|
+
if (intent?.kind !== 'blocked-labels')
|
|
2496
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2497
|
+
try {
|
|
2498
|
+
active = await this.persist(active, { intent: { ...intent, expected } });
|
|
2499
|
+
}
|
|
2500
|
+
catch {
|
|
2501
|
+
throw new PostEffectStateError(active);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
try {
|
|
2506
|
+
return await this.persistTerminal(active, outcome, evidenceCode, false);
|
|
2507
|
+
}
|
|
2508
|
+
catch {
|
|
2509
|
+
throw new PostEffectStateError(active);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
async reconcilePersistedBlockedTerminal(active, issue, outcome) {
|
|
2513
|
+
if (!this.dependencies.issues.transitionToBlocked) {
|
|
2514
|
+
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2515
|
+
}
|
|
2516
|
+
try {
|
|
2517
|
+
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
|
|
2521
|
+
}
|
|
2522
|
+
const observation = await this.readIssue(active.record.issueNumber);
|
|
2523
|
+
if (!observation || observation.state !== 'OPEN'
|
|
2524
|
+
|| blockedLabelProjection(observation.labels, active.config).status !== 'settled') {
|
|
2525
|
+
return this.invokedFailure(active, 'blocked-labels-observation-diverged');
|
|
2526
|
+
}
|
|
2527
|
+
return publicOutcome(outcome);
|
|
2528
|
+
}
|
|
2529
|
+
async persistTerminal(active, outcome, evidenceCode, retainIntent) {
|
|
2255
2530
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
|
|
2256
2531
|
const terminalOutcome = { ...outcome, evidencePath: evidence.path };
|
|
2257
2532
|
const changes = {
|
|
@@ -2263,7 +2538,7 @@ export class RunIssue {
|
|
|
2263
2538
|
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
2264
2539
|
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
2265
2540
|
? { status: 'blocked', kind: outcome.kind }
|
|
2266
|
-
: { status: outcome.status });
|
|
2541
|
+
: { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
|
|
2267
2542
|
}
|
|
2268
2543
|
if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
|
|
2269
2544
|
if (active.record.waitingHuman.phase === 'resumed') {
|
|
@@ -2503,6 +2778,36 @@ function sameStrings(left, right) {
|
|
|
2503
2778
|
const b = [...right].sort();
|
|
2504
2779
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
2505
2780
|
}
|
|
2781
|
+
function blockedLabelPolicy(config) {
|
|
2782
|
+
return {
|
|
2783
|
+
auto: config.github.labels.auto.name,
|
|
2784
|
+
running: config.github.labels.running.name,
|
|
2785
|
+
blocked: config.github.labels.blocked.name,
|
|
2786
|
+
review: config.github.labels.review.name,
|
|
2787
|
+
waitingHuman: config.github.labels.waitingHuman.name,
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function blockedLabelProjection(labels, config) {
|
|
2791
|
+
const policy = blockedLabelPolicy(config);
|
|
2792
|
+
const present = new Set(labels);
|
|
2793
|
+
if (present.has(policy.review) || present.has(policy.waitingHuman))
|
|
2794
|
+
return { status: 'diverged' };
|
|
2795
|
+
const auto = present.has(policy.auto);
|
|
2796
|
+
const running = present.has(policy.running);
|
|
2797
|
+
const blocked = present.has(policy.blocked);
|
|
2798
|
+
if (running || (auto && !blocked))
|
|
2799
|
+
return { status: 'transition' };
|
|
2800
|
+
if (blocked)
|
|
2801
|
+
return { status: 'settled', expected: auto ? [policy.auto, policy.blocked].sort() : [policy.blocked] };
|
|
2802
|
+
return { status: 'settled', expected: [] };
|
|
2803
|
+
}
|
|
2804
|
+
function sameCheckPolicy(checks, policy) {
|
|
2805
|
+
const expected = Object.entries(policy).sort(([left], [right]) => left.localeCompare(right));
|
|
2806
|
+
const actual = checks.map((check) => [check.id, check.command])
|
|
2807
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
2808
|
+
return actual.length === expected.length
|
|
2809
|
+
&& actual.every(([id, command], index) => id === expected[index]?.[0] && command === expected[index]?.[1]);
|
|
2810
|
+
}
|
|
2506
2811
|
function sameFreshness(left, right) {
|
|
2507
2812
|
return left.headSha === right.headSha
|
|
2508
2813
|
&& left.indexTreeSha === right.indexTreeSha
|