codex-orchestrator 2.0.8 → 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 +12 -0
- package/README.md +6 -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 +2 -3
- package/dist/src/v2/checked-change.js.map +1 -1
- 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 +6 -2
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +251 -51
- 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 +1 -0
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +11 -3
- 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 +3 -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
|
@@ -913,7 +913,10 @@ export class RunIssue {
|
|
|
913
913
|
&& active.record.directReview?.status === 'clear';
|
|
914
914
|
const feedbackRepair = active.record.reviewFeedback?.phase === 'repairing'
|
|
915
915
|
&& active.record.directReview?.stage === 'review-repair';
|
|
916
|
-
|
|
916
|
+
const qualificationRecovery = active.record.lifecycle === 'implementing'
|
|
917
|
+
&& !active.record.directReview
|
|
918
|
+
&& !this.hasRecordedImplementationStart(active.record, config);
|
|
919
|
+
if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair && !qualificationRecovery) {
|
|
917
920
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
918
921
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
919
922
|
}
|
|
@@ -971,10 +974,10 @@ export class RunIssue {
|
|
|
971
974
|
}
|
|
972
975
|
if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
|
|
973
976
|
&& !active.record.directReview) {
|
|
974
|
-
const
|
|
975
|
-
if ('status' in
|
|
976
|
-
return
|
|
977
|
-
active =
|
|
977
|
+
const qualification = await this.qualifyChecks(active, config, issueSnapshot, frozenCriteria);
|
|
978
|
+
if ('status' in qualification)
|
|
979
|
+
return qualification;
|
|
980
|
+
active = qualification.active;
|
|
978
981
|
}
|
|
979
982
|
attemptLoop: while (true) {
|
|
980
983
|
if (!await this.authorized(active, config)) {
|
|
@@ -1007,7 +1010,7 @@ export class RunIssue {
|
|
|
1007
1010
|
const feedbackProjection = feedbackBatch
|
|
1008
1011
|
? projectReviewFeedbackBatch(feedbackBatch, active.record.directReview.targetRevision)
|
|
1009
1012
|
: undefined;
|
|
1010
|
-
const
|
|
1013
|
+
const implementationLaunch = feedbackBatch ? {
|
|
1011
1014
|
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
1012
1015
|
reviewFeedback: feedbackProjection.workerFeedback,
|
|
1013
1016
|
onPrepared: async (invocation) => {
|
|
@@ -1031,9 +1034,20 @@ export class RunIssue {
|
|
|
1031
1034
|
},
|
|
1032
1035
|
});
|
|
1033
1036
|
},
|
|
1034
|
-
} : {
|
|
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
|
+
};
|
|
1035
1048
|
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
1036
1049
|
let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
|
|
1050
|
+
operation: 'implementation',
|
|
1037
1051
|
runId,
|
|
1038
1052
|
worktreePath,
|
|
1039
1053
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1042,7 +1056,7 @@ export class RunIssue {
|
|
|
1042
1056
|
reworkFindings: active.record.reworkFindings,
|
|
1043
1057
|
repairOnly: false,
|
|
1044
1058
|
workflowGeneration: active.record.workflowGeneration,
|
|
1045
|
-
...
|
|
1059
|
+
...implementationLaunch,
|
|
1046
1060
|
});
|
|
1047
1061
|
recoveredFeedbackImplementation = undefined;
|
|
1048
1062
|
if (implementation.kind === 'safe-halt') {
|
|
@@ -1096,6 +1110,7 @@ export class RunIssue {
|
|
|
1096
1110
|
if (repairBlock)
|
|
1097
1111
|
return repairBlock;
|
|
1098
1112
|
implementation = await this.runImplementation({
|
|
1113
|
+
operation: 'implementation',
|
|
1099
1114
|
runId,
|
|
1100
1115
|
worktreePath,
|
|
1101
1116
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1104,7 +1119,7 @@ export class RunIssue {
|
|
|
1104
1119
|
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
1105
1120
|
repairOnly: true,
|
|
1106
1121
|
workflowGeneration: active.record.workflowGeneration,
|
|
1107
|
-
...
|
|
1122
|
+
...implementationLaunch,
|
|
1108
1123
|
});
|
|
1109
1124
|
if (implementation.kind === 'safe-halt') {
|
|
1110
1125
|
active = await this.persist(active, {
|
|
@@ -1157,6 +1172,7 @@ export class RunIssue {
|
|
|
1157
1172
|
if (repairBlock)
|
|
1158
1173
|
return repairBlock;
|
|
1159
1174
|
implementation = await this.runImplementation({
|
|
1175
|
+
operation: 'implementation',
|
|
1160
1176
|
runId,
|
|
1161
1177
|
worktreePath,
|
|
1162
1178
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1165,7 +1181,7 @@ export class RunIssue {
|
|
|
1165
1181
|
reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
|
|
1166
1182
|
repairOnly: true,
|
|
1167
1183
|
workflowGeneration: active.record.workflowGeneration,
|
|
1168
|
-
...
|
|
1184
|
+
...implementationLaunch,
|
|
1169
1185
|
});
|
|
1170
1186
|
if (implementation.kind === 'safe-halt') {
|
|
1171
1187
|
active = await this.persist(active, {
|
|
@@ -1261,7 +1277,8 @@ export class RunIssue {
|
|
|
1261
1277
|
return await this.invokedFailure(active, 'issue-verification-invalid', error instanceof Error ? error.message : undefined);
|
|
1262
1278
|
}
|
|
1263
1279
|
const configuredChecks = Object.entries(checkPolicy.checks);
|
|
1264
|
-
const reusableChecks = active.record.checks.filter((check) =>
|
|
1280
|
+
const reusableChecks = active.record.checks.filter((check) => check.status === 'passed'
|
|
1281
|
+
&& configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
1265
1282
|
if (reusableChecks.length !== active.record.checks.length) {
|
|
1266
1283
|
active = await this.persist(active, {
|
|
1267
1284
|
checks: reusableChecks,
|
|
@@ -1273,7 +1290,7 @@ export class RunIssue {
|
|
|
1273
1290
|
for (const [id, command] of configuredChecks) {
|
|
1274
1291
|
if (active.record.checks.some((check) => check.id === id
|
|
1275
1292
|
&& check.command === command
|
|
1276
|
-
&&
|
|
1293
|
+
&& check.status === 'passed'))
|
|
1277
1294
|
continue;
|
|
1278
1295
|
if (this.signal.aborted)
|
|
1279
1296
|
return await this.terminal(active, { status: 'cancelled' });
|
|
@@ -1289,14 +1306,9 @@ export class RunIssue {
|
|
|
1289
1306
|
}
|
|
1290
1307
|
if (this.signal.aborted)
|
|
1291
1308
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1292
|
-
const
|
|
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 };
|
|
1309
|
+
const row = { id, command, status: check.status, outputSha256: check.outputSha256 };
|
|
1298
1310
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
1299
|
-
if (status === 'failed') {
|
|
1311
|
+
if (check.status === 'failed') {
|
|
1300
1312
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1301
1313
|
return active.record.reviewFeedback?.activeBatch
|
|
1302
1314
|
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
@@ -1366,7 +1378,7 @@ export class RunIssue {
|
|
|
1366
1378
|
changedFiles: proofChangedFiles,
|
|
1367
1379
|
checks: active.record.checks.map((check) => ({
|
|
1368
1380
|
...check,
|
|
1369
|
-
status:
|
|
1381
|
+
status: 'passed',
|
|
1370
1382
|
})),
|
|
1371
1383
|
checkPolicySha256: sha256(canonicalJson(checkPolicy.checks)),
|
|
1372
1384
|
packageVersion: active.record.packageVersion,
|
|
@@ -1882,7 +1894,7 @@ export class RunIssue {
|
|
|
1882
1894
|
packageVersion: workflow.receipt.packageVersion,
|
|
1883
1895
|
workflowGeneration: structuredClone(workflow.receipt),
|
|
1884
1896
|
skillHashes: structuredClone(workflow.skillHashes),
|
|
1885
|
-
|
|
1897
|
+
checks: [], createdAt: now, updatedAt: now,
|
|
1886
1898
|
};
|
|
1887
1899
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1888
1900
|
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
@@ -1895,7 +1907,7 @@ export class RunIssue {
|
|
|
1895
1907
|
delete record.intent;
|
|
1896
1908
|
if (Object.hasOwn(changes, 'process') && changes.process === undefined)
|
|
1897
1909
|
delete record.process;
|
|
1898
|
-
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']) {
|
|
1899
1911
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1900
1912
|
delete record[key];
|
|
1901
1913
|
}
|
|
@@ -2197,10 +2209,8 @@ export class RunIssue {
|
|
|
2197
2209
|
}
|
|
2198
2210
|
return this.persist(starting, { directReview: withoutInvocation });
|
|
2199
2211
|
}
|
|
2200
|
-
async
|
|
2212
|
+
async qualifyChecks(starting, config, issue, frozenCriteria) {
|
|
2201
2213
|
let active = starting;
|
|
2202
|
-
if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0)
|
|
2203
|
-
return { active };
|
|
2204
2214
|
let checkPolicy;
|
|
2205
2215
|
try {
|
|
2206
2216
|
checkPolicy = resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks);
|
|
@@ -2210,43 +2220,233 @@ export class RunIssue {
|
|
|
2210
2220
|
return this.invokedFailure(active, 'issue-verification-invalid', error instanceof Error ? error.message : undefined);
|
|
2211
2221
|
}
|
|
2212
2222
|
const configuredChecks = Object.entries(checkPolicy.checks);
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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;
|
|
2223
2243
|
try {
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
signal: this.signal,
|
|
2244
|
+
recovered = await this.dependencies.implementationAgent.recover({
|
|
2245
|
+
runId: active.record.runId,
|
|
2246
|
+
canonicalRepository: active.record.canonicalRepository,
|
|
2247
|
+
invocation: pendingInvocation,
|
|
2229
2248
|
});
|
|
2230
2249
|
}
|
|
2231
|
-
catch
|
|
2232
|
-
|
|
2233
|
-
|
|
2250
|
+
catch {
|
|
2251
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-report-recovery-failed');
|
|
2252
|
+
}
|
|
2253
|
+
if (recovered) {
|
|
2254
|
+
const settled = await this.settleQualificationRepair(active, config, recovered);
|
|
2255
|
+
if ('status' in settled)
|
|
2256
|
+
return settled;
|
|
2257
|
+
active = settled.active;
|
|
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');
|
|
2234
2267
|
}
|
|
2235
|
-
active = await this.
|
|
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.');
|
|
2268
|
+
active = await this.clearQualificationRepairInvocation(active);
|
|
2237
2269
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2270
|
+
}
|
|
2271
|
+
if (!prior) {
|
|
2272
|
+
active = await this.persist(active, {
|
|
2273
|
+
checkQualification: {
|
|
2274
|
+
version: 1, checkPolicySha256, repairAttempts, checks: [],
|
|
2275
|
+
implementationStarted: false, repairInvocation: null,
|
|
2276
|
+
},
|
|
2277
|
+
baselineChecks: undefined,
|
|
2278
|
+
});
|
|
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
|
+
}
|
|
2240
2307
|
}
|
|
2241
2308
|
active = await this.persist(active, {
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2309
|
+
checkQualification: {
|
|
2310
|
+
version: 1, checkPolicySha256, repairAttempts, checks,
|
|
2311
|
+
implementationStarted: false, repairInvocation: null,
|
|
2312
|
+
},
|
|
2313
|
+
baselineChecks: undefined,
|
|
2246
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.');
|
|
2247
2426
|
}
|
|
2248
2427
|
return { active };
|
|
2249
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
|
+
}
|
|
2250
2450
|
async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
|
|
2251
2451
|
const directReview = active.record.directReview;
|
|
2252
2452
|
const allowLegacyMalformed = directReview?.terminalCode === undefined
|