codex-orchestrator 2.0.7 → 2.0.9
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 +24 -0
- package/README.md +9 -6
- 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 +2 -3
- package/dist/src/v2/checked-change.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/review-feedback.d.ts +1 -0
- package/dist/src/v2/review-feedback.d.ts.map +1 -1
- package/dist/src/v2/review-feedback.js +1 -1
- package/dist/src/v2/review-feedback.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +7 -2
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +277 -55
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +17 -1
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +39 -1
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +6 -0
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +81 -14
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/workflow-assets.d.ts.map +1 -1
- package/dist/src/v2/workflow-assets.js +5 -4
- package/dist/src/v2/workflow-assets.js.map +1 -1
- package/docs/deep-dive.md +7 -3
- package/internal-workflow/docs/agents/contract-test-ledger.md +1 -0
- package/internal-workflow/evals/coding-skill-evals.json +276 -28
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/operations/qualification-repair/SKILL.md +17 -0
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +3 -0
- package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +1 -0
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +2 -0
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +9 -5
- package/internal-workflow/skills/small-task-implementer/SKILL.md +11 -3
- package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +2 -2
- package/package.json +1 -1
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -5,6 +5,7 @@ import { canonicalJson, sha256 } from './containment.js';
|
|
|
5
5
|
import { validateImplementationReport } from './implementation-report.js';
|
|
6
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';
|
|
@@ -912,7 +913,10 @@ export class RunIssue {
|
|
|
912
913
|
&& active.record.directReview?.status === 'clear';
|
|
913
914
|
const feedbackRepair = active.record.reviewFeedback?.phase === 'repairing'
|
|
914
915
|
&& active.record.directReview?.stage === 'review-repair';
|
|
915
|
-
|
|
916
|
+
const qualificationRecovery = active.record.lifecycle === 'implementing'
|
|
917
|
+
&& !active.record.directReview
|
|
918
|
+
&& !this.hasRecordedImplementationStart(active.record, config);
|
|
919
|
+
if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair && !qualificationRecovery) {
|
|
916
920
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
917
921
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
918
922
|
}
|
|
@@ -970,10 +974,10 @@ export class RunIssue {
|
|
|
970
974
|
}
|
|
971
975
|
if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
|
|
972
976
|
&& !active.record.directReview) {
|
|
973
|
-
const
|
|
974
|
-
if ('status' in
|
|
975
|
-
return
|
|
976
|
-
active =
|
|
977
|
+
const qualification = await this.qualifyChecks(active, config, issueSnapshot, frozenCriteria);
|
|
978
|
+
if ('status' in qualification)
|
|
979
|
+
return qualification;
|
|
980
|
+
active = qualification.active;
|
|
977
981
|
}
|
|
978
982
|
attemptLoop: while (true) {
|
|
979
983
|
if (!await this.authorized(active, config)) {
|
|
@@ -1006,7 +1010,7 @@ export class RunIssue {
|
|
|
1006
1010
|
const feedbackProjection = feedbackBatch
|
|
1007
1011
|
? projectReviewFeedbackBatch(feedbackBatch, active.record.directReview.targetRevision)
|
|
1008
1012
|
: undefined;
|
|
1009
|
-
const
|
|
1013
|
+
const implementationLaunch = feedbackBatch ? {
|
|
1010
1014
|
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
1011
1015
|
reviewFeedback: feedbackProjection.workerFeedback,
|
|
1012
1016
|
onPrepared: async (invocation) => {
|
|
@@ -1030,9 +1034,20 @@ export class RunIssue {
|
|
|
1030
1034
|
},
|
|
1031
1035
|
});
|
|
1032
1036
|
},
|
|
1033
|
-
} : {
|
|
1037
|
+
} : {
|
|
1038
|
+
onLaunched: async () => {
|
|
1039
|
+
const currentActive = active;
|
|
1040
|
+
const qualification = currentActive.record.checkQualification;
|
|
1041
|
+
if (!qualification)
|
|
1042
|
+
throw new Error('implementation launch requires check qualification');
|
|
1043
|
+
active = await this.persist(currentActive, {
|
|
1044
|
+
checkQualification: { ...qualification, implementationStarted: true },
|
|
1045
|
+
});
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
1034
1048
|
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
1035
1049
|
let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
|
|
1050
|
+
operation: 'implementation',
|
|
1036
1051
|
runId,
|
|
1037
1052
|
worktreePath,
|
|
1038
1053
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1041,7 +1056,7 @@ export class RunIssue {
|
|
|
1041
1056
|
reworkFindings: active.record.reworkFindings,
|
|
1042
1057
|
repairOnly: false,
|
|
1043
1058
|
workflowGeneration: active.record.workflowGeneration,
|
|
1044
|
-
...
|
|
1059
|
+
...implementationLaunch,
|
|
1045
1060
|
});
|
|
1046
1061
|
recoveredFeedbackImplementation = undefined;
|
|
1047
1062
|
if (implementation.kind === 'safe-halt') {
|
|
@@ -1095,6 +1110,7 @@ export class RunIssue {
|
|
|
1095
1110
|
if (repairBlock)
|
|
1096
1111
|
return repairBlock;
|
|
1097
1112
|
implementation = await this.runImplementation({
|
|
1113
|
+
operation: 'implementation',
|
|
1098
1114
|
runId,
|
|
1099
1115
|
worktreePath,
|
|
1100
1116
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1103,7 +1119,7 @@ export class RunIssue {
|
|
|
1103
1119
|
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
1104
1120
|
repairOnly: true,
|
|
1105
1121
|
workflowGeneration: active.record.workflowGeneration,
|
|
1106
|
-
...
|
|
1122
|
+
...implementationLaunch,
|
|
1107
1123
|
});
|
|
1108
1124
|
if (implementation.kind === 'safe-halt') {
|
|
1109
1125
|
active = await this.persist(active, {
|
|
@@ -1156,6 +1172,7 @@ export class RunIssue {
|
|
|
1156
1172
|
if (repairBlock)
|
|
1157
1173
|
return repairBlock;
|
|
1158
1174
|
implementation = await this.runImplementation({
|
|
1175
|
+
operation: 'implementation',
|
|
1159
1176
|
runId,
|
|
1160
1177
|
worktreePath,
|
|
1161
1178
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1164,7 +1181,7 @@ export class RunIssue {
|
|
|
1164
1181
|
reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
|
|
1165
1182
|
repairOnly: true,
|
|
1166
1183
|
workflowGeneration: active.record.workflowGeneration,
|
|
1167
|
-
...
|
|
1184
|
+
...implementationLaunch,
|
|
1168
1185
|
});
|
|
1169
1186
|
if (implementation.kind === 'safe-halt') {
|
|
1170
1187
|
active = await this.persist(active, {
|
|
@@ -1252,8 +1269,16 @@ export class RunIssue {
|
|
|
1252
1269
|
});
|
|
1253
1270
|
}
|
|
1254
1271
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1272
|
+
let checkPolicy;
|
|
1273
|
+
try {
|
|
1274
|
+
checkPolicy = resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks);
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
return await this.invokedFailure(active, 'issue-verification-invalid', error instanceof Error ? error.message : undefined);
|
|
1278
|
+
}
|
|
1279
|
+
const configuredChecks = Object.entries(checkPolicy.checks);
|
|
1280
|
+
const reusableChecks = active.record.checks.filter((check) => check.status === 'passed'
|
|
1281
|
+
&& configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
1257
1282
|
if (reusableChecks.length !== active.record.checks.length) {
|
|
1258
1283
|
active = await this.persist(active, {
|
|
1259
1284
|
checks: reusableChecks,
|
|
@@ -1262,30 +1287,28 @@ export class RunIssue {
|
|
|
1262
1287
|
proofReceipt: undefined,
|
|
1263
1288
|
});
|
|
1264
1289
|
}
|
|
1265
|
-
for (const [id, command] of
|
|
1290
|
+
for (const [id, command] of configuredChecks) {
|
|
1266
1291
|
if (active.record.checks.some((check) => check.id === id
|
|
1267
1292
|
&& check.command === command
|
|
1268
|
-
&&
|
|
1293
|
+
&& check.status === 'passed'))
|
|
1269
1294
|
continue;
|
|
1270
1295
|
if (this.signal.aborted)
|
|
1271
1296
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1272
1297
|
let check;
|
|
1273
1298
|
try {
|
|
1274
|
-
check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, phase: 'changed', signal: this.signal });
|
|
1299
|
+
check = await this.dependencies.checks.run({ id, command, source: checkPolicy.source, cwd: worktreePath, phase: 'changed', signal: this.signal });
|
|
1275
1300
|
}
|
|
1276
|
-
catch {
|
|
1277
|
-
|
|
1301
|
+
catch (error) {
|
|
1302
|
+
if (error instanceof CheckProcessQuiescenceError) {
|
|
1303
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'check-process-quiescence-unconfirmed');
|
|
1304
|
+
}
|
|
1305
|
+
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.');
|
|
1278
1306
|
}
|
|
1279
1307
|
if (this.signal.aborted)
|
|
1280
1308
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1281
|
-
const
|
|
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 };
|
|
1309
|
+
const row = { id, command, status: check.status, outputSha256: check.outputSha256 };
|
|
1287
1310
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
1288
|
-
if (status === 'failed') {
|
|
1311
|
+
if (check.status === 'failed') {
|
|
1289
1312
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1290
1313
|
return active.record.reviewFeedback?.activeBatch
|
|
1291
1314
|
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
@@ -1337,7 +1360,7 @@ export class RunIssue {
|
|
|
1337
1360
|
|| reviewedStageBinding.contentSha256 !== proofContentSha256) {
|
|
1338
1361
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
|
|
1339
1362
|
}
|
|
1340
|
-
if (!sameCheckPolicy(active.record.checks,
|
|
1363
|
+
if (!sameCheckPolicy(active.record.checks, checkPolicy.checks)) {
|
|
1341
1364
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
|
|
1342
1365
|
}
|
|
1343
1366
|
const payload = {
|
|
@@ -1355,9 +1378,9 @@ export class RunIssue {
|
|
|
1355
1378
|
changedFiles: proofChangedFiles,
|
|
1356
1379
|
checks: active.record.checks.map((check) => ({
|
|
1357
1380
|
...check,
|
|
1358
|
-
status:
|
|
1381
|
+
status: 'passed',
|
|
1359
1382
|
})),
|
|
1360
|
-
checkPolicySha256: sha256(canonicalJson(
|
|
1383
|
+
checkPolicySha256: sha256(canonicalJson(checkPolicy.checks)),
|
|
1361
1384
|
packageVersion: active.record.packageVersion,
|
|
1362
1385
|
proofSchemaVersion: 1,
|
|
1363
1386
|
};
|
|
@@ -1871,7 +1894,7 @@ export class RunIssue {
|
|
|
1871
1894
|
packageVersion: workflow.receipt.packageVersion,
|
|
1872
1895
|
workflowGeneration: structuredClone(workflow.receipt),
|
|
1873
1896
|
skillHashes: structuredClone(workflow.skillHashes),
|
|
1874
|
-
|
|
1897
|
+
checks: [], createdAt: now, updatedAt: now,
|
|
1875
1898
|
};
|
|
1876
1899
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1877
1900
|
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
@@ -1884,7 +1907,7 @@ export class RunIssue {
|
|
|
1884
1907
|
delete record.intent;
|
|
1885
1908
|
if (Object.hasOwn(changes, 'process') && changes.process === undefined)
|
|
1886
1909
|
delete record.process;
|
|
1887
|
-
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback']) {
|
|
1910
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback', 'checkQualification', 'baselineChecks']) {
|
|
1888
1911
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1889
1912
|
delete record[key];
|
|
1890
1913
|
}
|
|
@@ -2186,45 +2209,244 @@ export class RunIssue {
|
|
|
2186
2209
|
}
|
|
2187
2210
|
return this.persist(starting, { directReview: withoutInvocation });
|
|
2188
2211
|
}
|
|
2189
|
-
async
|
|
2212
|
+
async qualifyChecks(starting, config, issue, frozenCriteria) {
|
|
2190
2213
|
let active = starting;
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
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 });
|
|
2214
|
+
let checkPolicy;
|
|
2215
|
+
try {
|
|
2216
|
+
checkPolicy = resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks);
|
|
2197
2217
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2218
|
+
catch (error) {
|
|
2219
|
+
active = await this.persist(active, { lifecycle: 'routed' });
|
|
2220
|
+
return this.invokedFailure(active, 'issue-verification-invalid', error instanceof Error ? error.message : undefined);
|
|
2221
|
+
}
|
|
2222
|
+
const configuredChecks = Object.entries(checkPolicy.checks);
|
|
2223
|
+
const checkPolicySha256 = sha256(canonicalJson(checkPolicy.checks));
|
|
2224
|
+
const existingQualification = active.record.checkQualification;
|
|
2225
|
+
const prior = existingQualification?.checkPolicySha256 === checkPolicySha256
|
|
2226
|
+
? existingQualification
|
|
2227
|
+
: undefined;
|
|
2228
|
+
let repairAttempts = prior?.repairAttempts ?? 0;
|
|
2229
|
+
const pendingInvocation = existingQualification?.repairInvocation ?? null;
|
|
2230
|
+
if (pendingInvocation) {
|
|
2231
|
+
if (pendingInvocation.phase === 'launched') {
|
|
2232
|
+
try {
|
|
2233
|
+
await this.dependencies.waitForReviewProcessAbsence(pendingInvocation.processGroupId);
|
|
2234
|
+
}
|
|
2235
|
+
catch {
|
|
2236
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-process-absence-unconfirmed');
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
if (!this.dependencies.implementationAgent.recover) {
|
|
2240
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-report-recovery-unavailable');
|
|
2241
|
+
}
|
|
2242
|
+
let recovered;
|
|
2204
2243
|
try {
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
phase: 'baseline',
|
|
2210
|
-
signal: this.signal,
|
|
2244
|
+
recovered = await this.dependencies.implementationAgent.recover({
|
|
2245
|
+
runId: active.record.runId,
|
|
2246
|
+
canonicalRepository: active.record.canonicalRepository,
|
|
2247
|
+
invocation: pendingInvocation,
|
|
2211
2248
|
});
|
|
2212
2249
|
}
|
|
2213
2250
|
catch {
|
|
2214
|
-
|
|
2251
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-report-recovery-failed');
|
|
2215
2252
|
}
|
|
2216
|
-
if (
|
|
2217
|
-
|
|
2253
|
+
if (recovered) {
|
|
2254
|
+
const settled = await this.settleQualificationRepair(active, config, recovered);
|
|
2255
|
+
if ('status' in settled)
|
|
2256
|
+
return settled;
|
|
2257
|
+
active = settled.active;
|
|
2218
2258
|
}
|
|
2259
|
+
else {
|
|
2260
|
+
const deniedPathsBaseline = active.record.checkQualification?.deniedPathsBaseline;
|
|
2261
|
+
if (!deniedPathsBaseline
|
|
2262
|
+
|| await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
2263
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'denied-path-modified');
|
|
2264
|
+
}
|
|
2265
|
+
if (!sameFreshness(await this.dependencies.git.snapshot(active.record.worktreePath), pendingInvocation.baseline)) {
|
|
2266
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-worktree-ambiguous');
|
|
2267
|
+
}
|
|
2268
|
+
active = await this.clearQualificationRepairInvocation(active);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (!prior) {
|
|
2219
2272
|
active = await this.persist(active, {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2273
|
+
checkQualification: {
|
|
2274
|
+
version: 1, checkPolicySha256, repairAttempts, checks: [],
|
|
2275
|
+
implementationStarted: false, repairInvocation: null,
|
|
2276
|
+
},
|
|
2277
|
+
baselineChecks: undefined,
|
|
2224
2278
|
});
|
|
2225
2279
|
}
|
|
2280
|
+
while (true) {
|
|
2281
|
+
const checks = [];
|
|
2282
|
+
const failures = [];
|
|
2283
|
+
for (const [id, command] of configuredChecks) {
|
|
2284
|
+
if (this.signal.aborted)
|
|
2285
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
2286
|
+
let check;
|
|
2287
|
+
try {
|
|
2288
|
+
check = await this.dependencies.checks.run({
|
|
2289
|
+
id, command, source: checkPolicy.source,
|
|
2290
|
+
cwd: active.record.worktreePath,
|
|
2291
|
+
phase: 'qualification',
|
|
2292
|
+
signal: this.signal,
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
catch (error) {
|
|
2296
|
+
if (error instanceof CheckProcessQuiescenceError) {
|
|
2297
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'check-process-quiescence-unconfirmed');
|
|
2298
|
+
}
|
|
2299
|
+
return this.invokedFailure(active, 'qualification-check-execution-failed', error instanceof Error ? error.message : 'The qualification check process did not start or settle. Retry the same run.');
|
|
2300
|
+
}
|
|
2301
|
+
if (this.signal.aborted)
|
|
2302
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
2303
|
+
checks.push({ id, command, status: check.status, outputSha256: check.outputSha256 });
|
|
2304
|
+
if (check.status === 'failed') {
|
|
2305
|
+
failures.push(`Pre-implementation scoped check ${id} failed (${command}):\n${check.output.toString('utf8').slice(0, 8 * 1024)}`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
active = await this.persist(active, {
|
|
2309
|
+
checkQualification: {
|
|
2310
|
+
version: 1, checkPolicySha256, repairAttempts, checks,
|
|
2311
|
+
implementationStarted: false, repairInvocation: null,
|
|
2312
|
+
},
|
|
2313
|
+
baselineChecks: undefined,
|
|
2314
|
+
});
|
|
2315
|
+
if (failures.length === 0)
|
|
2316
|
+
return { active };
|
|
2317
|
+
if (repairAttempts >= config.runner.maxCycles) {
|
|
2318
|
+
return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'qualification-repair-exhausted');
|
|
2319
|
+
}
|
|
2320
|
+
if (!await this.authorized(active, config)) {
|
|
2321
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
2322
|
+
}
|
|
2323
|
+
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths);
|
|
2324
|
+
const repair = await this.runImplementation({
|
|
2325
|
+
operation: 'qualification-repair',
|
|
2326
|
+
runId: active.record.runId,
|
|
2327
|
+
worktreePath: active.record.worktreePath,
|
|
2328
|
+
issue,
|
|
2329
|
+
frozenCriteria,
|
|
2330
|
+
cycle: repairAttempts + 1,
|
|
2331
|
+
reworkFindings: failures,
|
|
2332
|
+
repairOnly: false,
|
|
2333
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
2334
|
+
onPrepared: async (invocation) => {
|
|
2335
|
+
active = await this.persist(active, {
|
|
2336
|
+
checkQualification: {
|
|
2337
|
+
...active.record.checkQualification,
|
|
2338
|
+
implementationStarted: false,
|
|
2339
|
+
deniedPathsBaseline,
|
|
2340
|
+
repairInvocation: {
|
|
2341
|
+
...structuredClone(invocation), phase: 'prepared',
|
|
2342
|
+
pid: null, processGroupId: null, launchedAt: null,
|
|
2343
|
+
},
|
|
2344
|
+
},
|
|
2345
|
+
});
|
|
2346
|
+
},
|
|
2347
|
+
onLaunched: async (invocation) => {
|
|
2348
|
+
const current = active.record.checkQualification?.repairInvocation;
|
|
2349
|
+
if (!current || current.phase !== 'prepared' || current.attemptId !== invocation.attemptId) {
|
|
2350
|
+
throw new Error('qualification repair launch mismatch');
|
|
2351
|
+
}
|
|
2352
|
+
repairAttempts = (repairAttempts + 1);
|
|
2353
|
+
active = await this.persist(active, {
|
|
2354
|
+
checkQualification: {
|
|
2355
|
+
...active.record.checkQualification, repairAttempts,
|
|
2356
|
+
repairInvocation: {
|
|
2357
|
+
...current, phase: 'launched', pid: invocation.pid,
|
|
2358
|
+
processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt,
|
|
2359
|
+
},
|
|
2360
|
+
},
|
|
2361
|
+
});
|
|
2362
|
+
},
|
|
2363
|
+
});
|
|
2364
|
+
if (repair.kind === 'safe-halt') {
|
|
2365
|
+
active = await this.persist(active, {
|
|
2366
|
+
lifecycle: 'safe-halt',
|
|
2367
|
+
process: {
|
|
2368
|
+
...repair.process,
|
|
2369
|
+
purpose: 'implementation',
|
|
2370
|
+
resumeLifecycle: 'implementing',
|
|
2371
|
+
resumeReviewStage: null,
|
|
2372
|
+
},
|
|
2373
|
+
});
|
|
2374
|
+
while (true) {
|
|
2375
|
+
try {
|
|
2376
|
+
await repair.waitForAbsence();
|
|
2377
|
+
break;
|
|
2378
|
+
}
|
|
2379
|
+
catch {
|
|
2380
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
2384
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
2385
|
+
}
|
|
2386
|
+
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
2387
|
+
}
|
|
2388
|
+
if (repair.kind !== 'completed'
|
|
2389
|
+
&& await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
2390
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
2391
|
+
}
|
|
2392
|
+
if (repair.kind === 'cancelled')
|
|
2393
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
2394
|
+
if (repair.kind !== 'completed') {
|
|
2395
|
+
return this.invokedFailure(active, 'qualification-repair-agent-failed', 'Qualification repair did not complete. Retry the same run.');
|
|
2396
|
+
}
|
|
2397
|
+
const settled = await this.settleQualificationRepair(active, config, repair);
|
|
2398
|
+
if ('status' in settled)
|
|
2399
|
+
return settled;
|
|
2400
|
+
active = settled.active;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
async settleQualificationRepair(active, config, repair) {
|
|
2404
|
+
const deniedPathsBaseline = active.record.checkQualification?.deniedPathsBaseline;
|
|
2405
|
+
if (!deniedPathsBaseline
|
|
2406
|
+
|| await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
2407
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'denied-path-modified');
|
|
2408
|
+
}
|
|
2409
|
+
active = await this.clearQualificationRepairInvocation(active);
|
|
2410
|
+
let report;
|
|
2411
|
+
try {
|
|
2412
|
+
report = validateImplementationReport(repair.report);
|
|
2413
|
+
}
|
|
2414
|
+
catch {
|
|
2415
|
+
return this.invokedFailure(active, 'qualification-repair-report-invalid', 'Qualification repair report was invalid. Retry the same run.');
|
|
2416
|
+
}
|
|
2417
|
+
if (report.status === 'external-block') {
|
|
2418
|
+
return this.terminal(active, { status: 'blocked', kind: 'external', resumable: true }, 'qualification-repair-external-block');
|
|
2419
|
+
}
|
|
2420
|
+
if (await this.dependencies.git.getHead(active.record.worktreePath) !== expectedImplementationHead(active.record)) {
|
|
2421
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'qualification-repair-head-changed');
|
|
2422
|
+
}
|
|
2423
|
+
const changedFiles = await this.dependencies.git.listChangedFiles(active.record.worktreePath);
|
|
2424
|
+
if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
|
|
2425
|
+
return this.invokedFailure(active, 'qualification-repair-change-set-invalid', 'Qualification repair must report the complete cumulative worktree change set.');
|
|
2426
|
+
}
|
|
2226
2427
|
return { active };
|
|
2227
2428
|
}
|
|
2429
|
+
async clearQualificationRepairInvocation(active) {
|
|
2430
|
+
const qualification = { ...active.record.checkQualification, implementationStarted: false, repairInvocation: null };
|
|
2431
|
+
delete qualification.deniedPathsBaseline;
|
|
2432
|
+
return this.persist(active, { checkQualification: qualification });
|
|
2433
|
+
}
|
|
2434
|
+
hasRecordedImplementationStart(record, config) {
|
|
2435
|
+
const qualification = record.checkQualification;
|
|
2436
|
+
if (!qualification)
|
|
2437
|
+
return false;
|
|
2438
|
+
let policy;
|
|
2439
|
+
try {
|
|
2440
|
+
policy = resolveIssueCheckPolicy(record.issueSnapshot.body, config.checks);
|
|
2441
|
+
}
|
|
2442
|
+
catch {
|
|
2443
|
+
return false;
|
|
2444
|
+
}
|
|
2445
|
+
return qualification.implementationStarted === true
|
|
2446
|
+
&& qualification.checkPolicySha256 === sha256(canonicalJson(policy.checks))
|
|
2447
|
+
&& sameCheckPolicy(qualification.checks, policy.checks)
|
|
2448
|
+
&& qualification.checks.every((check) => check.status === 'passed');
|
|
2449
|
+
}
|
|
2228
2450
|
async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
|
|
2229
2451
|
const directReview = active.record.directReview;
|
|
2230
2452
|
const allowLegacyMalformed = directReview?.terminalCode === undefined
|