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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +6 -4
  3. package/dist/src/v2/checked-change.d.ts +1 -1
  4. package/dist/src/v2/checked-change.d.ts.map +1 -1
  5. package/dist/src/v2/checked-change.js +2 -3
  6. package/dist/src/v2/checked-change.js.map +1 -1
  7. package/dist/src/v2/review-feedback.d.ts +1 -0
  8. package/dist/src/v2/review-feedback.d.ts.map +1 -1
  9. package/dist/src/v2/review-feedback.js +1 -1
  10. package/dist/src/v2/review-feedback.js.map +1 -1
  11. package/dist/src/v2/run-issue.d.ts +6 -2
  12. package/dist/src/v2/run-issue.d.ts.map +1 -1
  13. package/dist/src/v2/run-issue.js +251 -51
  14. package/dist/src/v2/run-issue.js.map +1 -1
  15. package/dist/src/v2/run-store.d.ts +17 -1
  16. package/dist/src/v2/run-store.d.ts.map +1 -1
  17. package/dist/src/v2/run-store.js +39 -1
  18. package/dist/src/v2/run-store.js.map +1 -1
  19. package/dist/src/v2/runtime.d.ts +1 -0
  20. package/dist/src/v2/runtime.d.ts.map +1 -1
  21. package/dist/src/v2/runtime.js +11 -3
  22. package/dist/src/v2/runtime.js.map +1 -1
  23. package/dist/src/v2/workflow-assets.d.ts.map +1 -1
  24. package/dist/src/v2/workflow-assets.js +5 -4
  25. package/dist/src/v2/workflow-assets.js.map +1 -1
  26. package/docs/deep-dive.md +3 -3
  27. package/internal-workflow/docs/agents/contract-test-ledger.md +1 -0
  28. package/internal-workflow/evals/coding-skill-evals.json +276 -28
  29. package/internal-workflow/manifest.json +1 -1
  30. package/internal-workflow/operations/qualification-repair/SKILL.md +17 -0
  31. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +3 -0
  32. package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +1 -0
  33. package/internal-workflow/skills/implementation-spec-review/SKILL.md +2 -0
  34. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
  35. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +9 -5
  36. package/internal-workflow/skills/small-task-implementer/SKILL.md +11 -3
  37. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +2 -2
  38. package/package.json +1 -1
@@ -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
- if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair) {
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 baseline = await this.captureBaselineChecks(active, config);
975
- if ('status' in baseline)
976
- return baseline;
977
- active = baseline.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 feedbackLaunch = feedbackBatch ? {
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
- ...feedbackLaunch,
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
- ...feedbackLaunch,
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
- ...feedbackLaunch,
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) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
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
- && (check.status === 'passed' || check.status === 'unchanged-failure')))
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 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 };
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: check.status === 'unchanged-failure' ? 'unchanged-failure' : 'passed',
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
- baselineChecks: [], checks: [], createdAt: now, updatedAt: now,
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 captureBaselineChecks(starting, config) {
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 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
+ 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
- check = await this.dependencies.checks.run({
2225
- id, command, source: checkPolicy.source,
2226
- cwd: active.record.worktreePath,
2227
- phase: 'baseline',
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 (error) {
2232
- if (error instanceof CheckProcessQuiescenceError) {
2233
- return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'check-process-quiescence-unconfirmed');
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.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.');
2268
+ active = await this.clearQualificationRepairInvocation(active);
2237
2269
  }
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');
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
- baselineChecks: [
2243
- ...(active.record.baselineChecks ?? []),
2244
- { id, command, status: check.status, outputSha256: check.outputSha256 },
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