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.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +9 -6
  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/issue-check-policy.d.ts +17 -0
  8. package/dist/src/v2/issue-check-policy.d.ts.map +1 -0
  9. package/dist/src/v2/issue-check-policy.js +133 -0
  10. package/dist/src/v2/issue-check-policy.js.map +1 -0
  11. package/dist/src/v2/review-feedback.d.ts +1 -0
  12. package/dist/src/v2/review-feedback.d.ts.map +1 -1
  13. package/dist/src/v2/review-feedback.js +1 -1
  14. package/dist/src/v2/review-feedback.js.map +1 -1
  15. package/dist/src/v2/run-issue.d.ts +7 -2
  16. package/dist/src/v2/run-issue.d.ts.map +1 -1
  17. package/dist/src/v2/run-issue.js +277 -55
  18. package/dist/src/v2/run-issue.js.map +1 -1
  19. package/dist/src/v2/run-store.d.ts +17 -1
  20. package/dist/src/v2/run-store.d.ts.map +1 -1
  21. package/dist/src/v2/run-store.js +39 -1
  22. package/dist/src/v2/run-store.js.map +1 -1
  23. package/dist/src/v2/runtime.d.ts +6 -0
  24. package/dist/src/v2/runtime.d.ts.map +1 -1
  25. package/dist/src/v2/runtime.js +81 -14
  26. package/dist/src/v2/runtime.js.map +1 -1
  27. package/dist/src/v2/workflow-assets.d.ts.map +1 -1
  28. package/dist/src/v2/workflow-assets.js +5 -4
  29. package/dist/src/v2/workflow-assets.js.map +1 -1
  30. package/docs/deep-dive.md +7 -3
  31. package/internal-workflow/docs/agents/contract-test-ledger.md +1 -0
  32. package/internal-workflow/evals/coding-skill-evals.json +276 -28
  33. package/internal-workflow/manifest.json +1 -1
  34. package/internal-workflow/operations/qualification-repair/SKILL.md +17 -0
  35. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +3 -0
  36. package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +1 -0
  37. package/internal-workflow/skills/implementation-spec-review/SKILL.md +2 -0
  38. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
  39. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +9 -5
  40. package/internal-workflow/skills/small-task-implementer/SKILL.md +11 -3
  41. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +2 -2
  42. package/package.json +1 -1
@@ -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
- 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) {
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 baseline = await this.captureBaselineChecks(active, config);
974
- if ('status' in baseline)
975
- return baseline;
976
- 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;
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 feedbackLaunch = feedbackBatch ? {
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
- ...feedbackLaunch,
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
- ...feedbackLaunch,
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
- ...feedbackLaunch,
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
- const configuredChecks = Object.entries(config.checks);
1256
- const reusableChecks = active.record.checks.filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
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 Object.entries(config.checks)) {
1290
+ for (const [id, command] of configuredChecks) {
1266
1291
  if (active.record.checks.some((check) => check.id === id
1267
1292
  && check.command === command
1268
- && (check.status === 'passed' || check.status === 'unchanged-failure')))
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
- return await this.terminal(active, { status: 'internal-error', code: 'configured-check-execution-failed' });
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 outputSha256 = check.outputSha256;
1282
- const baseline = active.record.baselineChecks?.find((candidate) => candidate.id === id && candidate.command === command);
1283
- const status = check.status === 'failed' && baseline?.status === 'failed' && baseline.outputSha256 === outputSha256
1284
- ? 'unchanged-failure'
1285
- : check.status;
1286
- const row = { id, command, status, outputSha256 };
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, config.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: check.status === 'unchanged-failure' ? 'unchanged-failure' : 'passed',
1381
+ status: 'passed',
1359
1382
  })),
1360
- checkPolicySha256: sha256(canonicalJson(config.checks)),
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
- baselineChecks: [], checks: [], createdAt: now, updatedAt: now,
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 captureBaselineChecks(starting, config) {
2212
+ async qualifyChecks(starting, config, issue, frozenCriteria) {
2190
2213
  let active = starting;
2191
- if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0)
2192
- return { active };
2193
- const configuredChecks = Object.entries(config.checks);
2194
- const reusableBaselineChecks = (active.record.baselineChecks ?? []).filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
2195
- if (reusableBaselineChecks.length !== (active.record.baselineChecks ?? []).length) {
2196
- active = await this.persist(active, { baselineChecks: reusableBaselineChecks });
2214
+ let checkPolicy;
2215
+ try {
2216
+ checkPolicy = resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks);
2197
2217
  }
2198
- for (const [id, command] of Object.entries(config.checks)) {
2199
- if (active.record.baselineChecks?.some((check) => check.id === id && check.command === command))
2200
- continue;
2201
- if (this.signal.aborted)
2202
- return this.terminal(active, { status: 'cancelled' });
2203
- let check;
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
- check = await this.dependencies.checks.run({
2206
- id,
2207
- command,
2208
- cwd: active.record.worktreePath,
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
- continue;
2251
+ return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-report-recovery-failed');
2215
2252
  }
2216
- if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0) {
2217
- return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'baseline-check-mutated-worktree');
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
- baselineChecks: [
2221
- ...(active.record.baselineChecks ?? []),
2222
- { id, command, status: check.status, outputSha256: check.outputSha256 },
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