codex-orchestrator 2.0.6 → 2.0.7

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 (41) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +5 -2
  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 +3 -2
  6. package/dist/src/v2/checked-change.js.map +1 -1
  7. package/dist/src/v2/code-review-report.d.ts +1 -2
  8. package/dist/src/v2/code-review-report.d.ts.map +1 -1
  9. package/dist/src/v2/code-review-report.js +6 -11
  10. package/dist/src/v2/code-review-report.js.map +1 -1
  11. package/dist/src/v2/containment.d.ts +0 -52
  12. package/dist/src/v2/containment.d.ts.map +1 -1
  13. package/dist/src/v2/containment.js +1 -195
  14. package/dist/src/v2/containment.js.map +1 -1
  15. package/dist/src/v2/direct-delivery.d.ts +11 -2
  16. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  17. package/dist/src/v2/direct-delivery.js +62 -17
  18. package/dist/src/v2/direct-delivery.js.map +1 -1
  19. package/dist/src/v2/implementation-reviewer.d.ts +1 -1
  20. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
  21. package/dist/src/v2/implementation-reviewer.js +1 -2
  22. package/dist/src/v2/implementation-reviewer.js.map +1 -1
  23. package/dist/src/v2/proof-report.d.ts +1 -1
  24. package/dist/src/v2/proof-report.d.ts.map +1 -1
  25. package/dist/src/v2/proof-report.js +6 -4
  26. package/dist/src/v2/proof-report.js.map +1 -1
  27. package/dist/src/v2/run-issue.d.ts +14 -1
  28. package/dist/src/v2/run-issue.d.ts.map +1 -1
  29. package/dist/src/v2/run-issue.js +348 -65
  30. package/dist/src/v2/run-issue.js.map +1 -1
  31. package/dist/src/v2/run-store.d.ts +14 -1
  32. package/dist/src/v2/run-store.d.ts.map +1 -1
  33. package/dist/src/v2/run-store.js +18 -3
  34. package/dist/src/v2/run-store.js.map +1 -1
  35. package/dist/src/v2/runtime.d.ts.map +1 -1
  36. package/dist/src/v2/runtime.js +21 -17
  37. package/dist/src/v2/runtime.js.map +1 -1
  38. package/docs/deep-dive.md +16 -10
  39. package/internal-workflow/manifest.json +1 -1
  40. package/internal-workflow/schemas/proof-report-v1.json +1 -1
  41. package/package.json +1 -2
@@ -3,7 +3,7 @@ import { checkedChangePayloadSha256, } from './checked-change.js';
3
3
  import { parseAgentAutoConfig } from './config.js';
4
4
  import { canonicalJson, sha256 } from './containment.js';
5
5
  import { validateImplementationReport } from './implementation-report.js';
6
- import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
6
+ import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, canRecoverTerminalDirectReviewReport, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, MAX_DIRECT_REVIEW_REPORT_REPAIRS, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, recoverTerminalDirectReviewReport, } from './direct-delivery.js';
7
7
  import { ProofLaunchAuthorizationError, ProofQuiescenceError } from './acceptance-proof.js';
8
8
  import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
9
9
  import { initialRouteExecution, } from './route-coordinator.js';
@@ -30,56 +30,88 @@ export class RunIssue {
30
30
  let observation = await this.readIssue(issueNumber);
31
31
  if (!observation || observation.state !== 'OPEN')
32
32
  return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
33
+ const body = claimComment(runId, issueNumber, branchName);
34
+ const marker = body.split('\n')[0];
35
+ const commentIntent = { kind: 'comment', issueNumber, marker, bodySha256: sha256(body) };
36
+ // Compatibility with runs persisted by the former label-first claim order.
37
+ // If the label effect is already visible, confirm it. Otherwise establish
38
+ // the trusted claim comment before granting the running status label.
33
39
  if (active.record.intent?.kind === 'claim-labels') {
34
- if (!sameStrings(observation.labels, expectedLabels)) {
40
+ if (sameStrings(observation.labels, expectedLabels)) {
41
+ active = await this.confirmEffect(active);
42
+ }
43
+ else if (!this.hasTrustedClaim(observation, active.record)) {
35
44
  const labels = new Set(observation.labels);
36
45
  if (!labels.has(config.github.labels.auto.name)
37
46
  || labels.has(config.github.labels.blocked.name)
38
- || labels.has(config.github.labels.review.name)) {
47
+ || labels.has(config.github.labels.review.name)
48
+ || labels.has(config.github.labels.waitingHuman.name)) {
39
49
  return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
40
50
  }
51
+ active = await this.persist(active, { intent: commentIntent });
52
+ }
53
+ }
54
+ if (!active.record.intent) {
55
+ observation = await this.readIssue(issueNumber);
56
+ if (!observation || observation.state !== 'OPEN')
57
+ return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
58
+ if (!this.hasTrustedClaim(observation, active.record)) {
59
+ active = await this.persist(active, { intent: commentIntent });
60
+ }
61
+ }
62
+ if (active.record.intent?.kind === 'comment') {
63
+ if (active.record.intent.marker !== marker || active.record.intent.bodySha256 !== sha256(body)) {
64
+ return { result: await this.publicationDiverged(active, 'claim-comment-intent-diverged') };
65
+ }
66
+ observation = await this.readIssue(issueNumber);
67
+ let comments = observation ? commentsWithMarker(observation, marker) : [];
68
+ if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
69
+ return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
70
+ }
71
+ if (comments.length === 0) {
41
72
  try {
42
- await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
73
+ await this.dependencies.issues.postComment(issueNumber, body);
43
74
  }
44
75
  catch {
45
- return { result: await this.invokedFailure(active, 'claim-labels-delivery-unknown') };
76
+ return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
46
77
  }
47
78
  observation = await this.readIssue(issueNumber);
79
+ comments = observation ? commentsWithMarker(observation, marker) : [];
48
80
  }
49
- if (!observation || !sameStrings(observation.labels, expectedLabels)) {
50
- return { result: await this.publicationDiverged(active, 'claim-labels-observation-diverged') };
81
+ if (comments.length !== 1 || comments[0].body !== body
82
+ || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comments[0].authorAssociation)) {
83
+ return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
51
84
  }
52
85
  active = await this.confirmEffect(active);
53
86
  }
54
- else if (!sameStrings(observation.labels, expectedLabels)) {
55
- return { result: await this.publicationDiverged(active, 'claim-labels-missing-before-comment') };
87
+ observation = await this.readIssue(issueNumber);
88
+ if (!observation || observation.state !== 'OPEN' || !this.hasTrustedClaim(observation, active.record)) {
89
+ return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
90
+ }
91
+ if (sameStrings(observation.labels, expectedLabels))
92
+ return { active };
93
+ const labels = new Set(observation.labels);
94
+ if (!labels.has(config.github.labels.auto.name)
95
+ || labels.has(config.github.labels.blocked.name)
96
+ || labels.has(config.github.labels.review.name)
97
+ || labels.has(config.github.labels.waitingHuman.name)) {
98
+ return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
56
99
  }
57
- const body = claimComment(runId, issueNumber, branchName);
58
- const marker = body.split('\n')[0];
59
100
  if (!active.record.intent) {
60
- active = await this.persist(active, { intent: { kind: 'comment', issueNumber, marker, bodySha256: sha256(body) } });
101
+ active = await this.persist(active, { intent: { kind: 'claim-labels', issueNumber, expected: expectedLabels } });
61
102
  }
62
- if (active.record.intent?.kind !== 'comment' || active.record.intent.marker !== marker || active.record.intent.bodySha256 !== sha256(body)) {
63
- return { result: await this.publicationDiverged(active, 'claim-comment-intent-diverged') };
103
+ if (active.record.intent?.kind !== 'claim-labels' || !sameStrings(active.record.intent.expected, expectedLabels)) {
104
+ return { result: await this.publicationDiverged(active, 'claim-labels-intent-diverged') };
64
105
  }
65
- observation = await this.readIssue(issueNumber);
66
- let comments = observation ? commentsWithMarker(observation, marker) : [];
67
- if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
68
- return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
106
+ try {
107
+ await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
69
108
  }
70
- if (comments.length === 0) {
71
- try {
72
- await this.dependencies.issues.postComment(issueNumber, body);
73
- }
74
- catch {
75
- return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
76
- }
77
- observation = await this.readIssue(issueNumber);
78
- comments = observation ? commentsWithMarker(observation, marker) : [];
109
+ catch {
110
+ return { result: await this.invokedFailure(active, 'claim-labels-delivery-unknown') };
79
111
  }
80
- if (comments.length !== 1 || comments[0].body !== body
81
- || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comments[0].authorAssociation)) {
82
- return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
112
+ observation = await this.readIssue(issueNumber);
113
+ if (!observation || !sameStrings(observation.labels, expectedLabels) || !this.hasTrustedClaim(observation, active.record)) {
114
+ return { result: await this.publicationDiverged(active, 'claim-labels-observation-diverged') };
83
115
  }
84
116
  active = await this.confirmEffect(active);
85
117
  return { active };
@@ -706,17 +738,6 @@ export class RunIssue {
706
738
  return await this.preClaimInternal('config-changed-during-owner-acquire', input.issueNumber);
707
739
  }
708
740
  const config = confirmedConfig.config;
709
- try {
710
- await this.dependencies.validateContainment(config);
711
- }
712
- catch {
713
- const evidence = await this.dependencies.writeEvidence({
714
- runId: `issue-${input.issueNumber}`,
715
- code: 'containment-certificate-invalid',
716
- summary: 'Containment certification is unavailable or stale.',
717
- });
718
- return { status: 'blocked', kind: 'safety', resumable: true, evidencePath: evidence.path };
719
- }
720
741
  if (this.signal.aborted)
721
742
  return await this.preClaimCancelled(input.issueNumber);
722
743
  let issue;
@@ -753,15 +774,33 @@ export class RunIssue {
753
774
  if (active.record.intent?.kind === 'review-blocked-labels') {
754
775
  return await this.blockReviewFeedback(active, active.record.intent.blockKind, active.record.intent.evidenceCode);
755
776
  }
777
+ if (active.record.intent?.kind === 'blocked-labels') {
778
+ return await this.publishBlockedTerminal(active, {
779
+ status: 'blocked',
780
+ kind: active.record.intent.blockKind,
781
+ resumable: active.record.intent.resumable,
782
+ }, active.record.intent.evidenceCode);
783
+ }
784
+ if (existing.terminalOutcome?.status === 'blocked'
785
+ && issue?.state === 'OPEN'
786
+ && blockedLabelProjection(issue.labels, config).status !== 'settled') {
787
+ return await this.reconcilePersistedBlockedTerminal(active, issue, existing.terminalOutcome);
788
+ }
756
789
  if (existing.terminalOutcome) {
757
- if (existing.terminalOutcome.status !== 'review-ready')
758
- return publicOutcome(existing.terminalOutcome);
759
- const continuation = await this.continueReviewReady(active, targetRoot, config);
760
- if ('result' in continuation)
761
- return continuation.result;
762
- active = continuation.active;
763
- issueSnapshot = structuredClone(active.record.issueSnapshot);
764
- frozenCriteria = structuredClone(active.record.frozenCriteria);
790
+ if (existing.terminalOutcome.status !== 'review-ready') {
791
+ const recovered = await this.recoverTerminalReviewReport(active, issue, config, frozenCriteria);
792
+ if (!recovered)
793
+ return publicOutcome(existing.terminalOutcome);
794
+ active = recovered;
795
+ }
796
+ else {
797
+ const continuation = await this.continueReviewReady(active, targetRoot, config);
798
+ if ('result' in continuation)
799
+ return continuation.result;
800
+ active = continuation.active;
801
+ issueSnapshot = structuredClone(active.record.issueSnapshot);
802
+ frozenCriteria = structuredClone(active.record.frozenCriteria);
803
+ }
765
804
  }
766
805
  if (active.record.reviewFeedback?.phase === 'frozen') {
767
806
  const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
@@ -896,11 +935,13 @@ export class RunIssue {
896
935
  worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${input.issueNumber}`);
897
936
  baseSha = await this.dependencies.git.getBaseSha({ targetRoot, baseBranch: config.github.baseBranch });
898
937
  assertGitSha(baseSha, 'baseSha');
899
- const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
938
+ const claimBody = claimComment(runId, input.issueNumber, branchName);
900
939
  active = await this.createRun({
901
940
  runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
902
941
  issueSnapshot, frozenCriteria, config,
903
- intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
942
+ intent: {
943
+ kind: 'comment', issueNumber: input.issueNumber, marker: claimBody.split('\n')[0], bodySha256: sha256(claimBody),
944
+ },
904
945
  });
905
946
  const claim = await this.reconcileClaim(active, config);
906
947
  if ('result' in claim)
@@ -927,6 +968,13 @@ export class RunIssue {
927
968
  if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
928
969
  return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
929
970
  }
971
+ if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
972
+ && !active.record.directReview) {
973
+ const baseline = await this.captureBaselineChecks(active, config);
974
+ if ('status' in baseline)
975
+ return baseline;
976
+ active = baseline.active;
977
+ }
930
978
  attemptLoop: while (true) {
931
979
  if (!await this.authorized(active, config)) {
932
980
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
@@ -1204,23 +1252,40 @@ export class RunIssue {
1204
1252
  });
1205
1253
  }
1206
1254
  }
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));
1257
+ if (reusableChecks.length !== active.record.checks.length) {
1258
+ active = await this.persist(active, {
1259
+ checks: reusableChecks,
1260
+ checkedChangeSha256: undefined,
1261
+ proofId: undefined,
1262
+ proofReceipt: undefined,
1263
+ });
1264
+ }
1207
1265
  for (const [id, command] of Object.entries(config.checks)) {
1208
- if (active.record.checks.some((check) => check.id === id && check.status === 'passed'))
1266
+ if (active.record.checks.some((check) => check.id === id
1267
+ && check.command === command
1268
+ && (check.status === 'passed' || check.status === 'unchanged-failure')))
1209
1269
  continue;
1210
1270
  if (this.signal.aborted)
1211
1271
  return await this.terminal(active, { status: 'cancelled' });
1212
1272
  let check;
1213
1273
  try {
1214
- check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, signal: this.signal });
1274
+ check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, phase: 'changed', signal: this.signal });
1215
1275
  }
1216
1276
  catch {
1217
1277
  return await this.terminal(active, { status: 'internal-error', code: 'configured-check-execution-failed' });
1218
1278
  }
1219
1279
  if (this.signal.aborted)
1220
1280
  return await this.terminal(active, { status: 'cancelled' });
1221
- const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
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 };
1222
1287
  active = await this.persist(active, { checks: [...active.record.checks, row] });
1223
- if (check.status !== 'passed') {
1288
+ if (status === 'failed') {
1224
1289
  if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
1225
1290
  return active.record.reviewFeedback?.activeBatch
1226
1291
  ? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
@@ -1272,6 +1337,9 @@ export class RunIssue {
1272
1337
  || reviewedStageBinding.contentSha256 !== proofContentSha256) {
1273
1338
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
1274
1339
  }
1340
+ if (!sameCheckPolicy(active.record.checks, config.checks)) {
1341
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
1342
+ }
1275
1343
  const payload = {
1276
1344
  version: 1,
1277
1345
  canonicalRepository,
@@ -1285,7 +1353,10 @@ export class RunIssue {
1285
1353
  untrackedContentSha256: freshness.untrackedContentSha256,
1286
1354
  worktreeIdentity: freshness.worktreeIdentity,
1287
1355
  changedFiles: proofChangedFiles,
1288
- checks: active.record.checks.map((check) => ({ ...check, status: 'passed' })),
1356
+ checks: active.record.checks.map((check) => ({
1357
+ ...check,
1358
+ status: check.status === 'unchanged-failure' ? 'unchanged-failure' : 'passed',
1359
+ })),
1289
1360
  checkPolicySha256: sha256(canonicalJson(config.checks)),
1290
1361
  packageVersion: active.record.packageVersion,
1291
1362
  proofSchemaVersion: 1,
@@ -1800,7 +1871,7 @@ export class RunIssue {
1800
1871
  packageVersion: workflow.receipt.packageVersion,
1801
1872
  workflowGeneration: structuredClone(workflow.receipt),
1802
1873
  skillHashes: structuredClone(workflow.skillHashes),
1803
- checks: [], createdAt: now, updatedAt: now,
1874
+ baselineChecks: [], checks: [], createdAt: now, updatedAt: now,
1804
1875
  };
1805
1876
  const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
1806
1877
  schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
@@ -1842,7 +1913,10 @@ export class RunIssue {
1842
1913
  if (issue.state !== 'OPEN')
1843
1914
  return false;
1844
1915
  const labels = new Set(issue.labels);
1845
- if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
1916
+ if (!labels.has(config.github.labels.auto.name)
1917
+ || labels.has(config.github.labels.blocked.name)
1918
+ || labels.has(config.github.labels.review.name)
1919
+ || labels.has(config.github.labels.waitingHuman.name))
1846
1920
  return false;
1847
1921
  return this.hasTrustedClaim(issue, record);
1848
1922
  }
@@ -1957,9 +2031,7 @@ export class RunIssue {
1957
2031
  affectedDefectIds: [...directReview.review.affectedDefectIds],
1958
2032
  fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
1959
2033
  .map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
1960
- mandatoryCoverage: directReview.stage === 'review-closure'
1961
- ? [...directReview.review.coverage]
1962
- : ['acceptance-criteria', 'correctness', 'test-quality'],
2034
+ reviewFocus: ['acceptance-criteria', 'correctness', 'test-quality'],
1963
2035
  workflowGeneration: structuredClone(active.record.workflowGeneration),
1964
2036
  repairOnly: reportRepair !== undefined,
1965
2037
  originalReportSha256: reportRepair?.originalReportSha256 ?? null,
@@ -2029,14 +2101,17 @@ export class RunIssue {
2029
2101
  const current = active.record.directReview;
2030
2102
  if (!current)
2031
2103
  return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
2032
- if (current.review.reportRepairs >= 1 || reportRepair) {
2033
- return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
2104
+ if (current.review.reportRepairs >= MAX_DIRECT_REVIEW_REPORT_REPAIRS) {
2105
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' }, 'direct-review-report-malformed');
2034
2106
  }
2035
2107
  const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
2036
2108
  active = await this.persist(active, {
2037
2109
  directReview: {
2038
2110
  ...withoutInvocation,
2039
- review: { ...withoutInvocation.review, reportRepairs: 1 },
2111
+ review: {
2112
+ ...withoutInvocation.review,
2113
+ reportRepairs: (withoutInvocation.review.reportRepairs + 1),
2114
+ },
2040
2115
  },
2041
2116
  });
2042
2117
  reportRepair = {
@@ -2111,6 +2186,89 @@ export class RunIssue {
2111
2186
  }
2112
2187
  return this.persist(starting, { directReview: withoutInvocation });
2113
2188
  }
2189
+ async captureBaselineChecks(starting, config) {
2190
+ 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 });
2197
+ }
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;
2204
+ try {
2205
+ check = await this.dependencies.checks.run({
2206
+ id,
2207
+ command,
2208
+ cwd: active.record.worktreePath,
2209
+ phase: 'baseline',
2210
+ signal: this.signal,
2211
+ });
2212
+ }
2213
+ catch {
2214
+ continue;
2215
+ }
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');
2218
+ }
2219
+ active = await this.persist(active, {
2220
+ baselineChecks: [
2221
+ ...(active.record.baselineChecks ?? []),
2222
+ { id, command, status: check.status, outputSha256: check.outputSha256 },
2223
+ ],
2224
+ });
2225
+ }
2226
+ return { active };
2227
+ }
2228
+ async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
2229
+ const directReview = active.record.directReview;
2230
+ const allowLegacyMalformed = directReview?.terminalCode === undefined
2231
+ && active.record.outcomeEvidenceId === `evidence:${active.record.runId}:direct-review-report-malformed`;
2232
+ if (!issue || !directReview || active.record.intent || active.record.process
2233
+ || active.record.terminalOutcome?.status !== 'internal-error'
2234
+ || directReview.status !== 'terminal' || directReview.terminalOutcome?.status !== 'internal-error'
2235
+ || !canRecoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed })
2236
+ || !this.isAuthorizedIssue(issue, active.record, config))
2237
+ return undefined;
2238
+ let worktree;
2239
+ try {
2240
+ worktree = await this.dependencies.git.inspectWorktree({
2241
+ worktreePath: active.record.worktreePath,
2242
+ branchName: active.record.branchName,
2243
+ baseSha: active.record.baseSha,
2244
+ });
2245
+ }
2246
+ catch {
2247
+ return undefined;
2248
+ }
2249
+ if (worktree !== 'matching'
2250
+ || await this.dependencies.git.getHead(active.record.worktreePath) !== expectedImplementationHead(active.record))
2251
+ return undefined;
2252
+ const changedFiles = await this.dependencies.git.listChangedFiles(active.record.worktreePath);
2253
+ if (changedFiles.length === 0 || !active.record.routeReceipt)
2254
+ return undefined;
2255
+ const targetFingerprint = directReviewTargetFingerprint({
2256
+ snapshot: await this.dependencies.git.snapshot(active.record.worktreePath),
2257
+ changedFiles,
2258
+ routeDecisionSha256: active.record.routeReceipt.decisionSha256,
2259
+ workflowGenerationHash: active.record.workflowGeneration.generationHash,
2260
+ cycle: active.record.cycle,
2261
+ frozenCriteria,
2262
+ });
2263
+ if (targetFingerprint !== directReview.targetFingerprint)
2264
+ return undefined;
2265
+ return this.persist(active, {
2266
+ lifecycle: 'implementing',
2267
+ directReview: recoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed }),
2268
+ terminalOutcome: undefined,
2269
+ outcomeEvidenceId: undefined,
2270
+ });
2271
+ }
2114
2272
  async startNextCycle(active, findings, sources) {
2115
2273
  if (active.record.directReview?.status === 'clear') {
2116
2274
  const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
@@ -2252,6 +2410,101 @@ export class RunIssue {
2252
2410
  if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
2253
2411
  return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
2254
2412
  }
2413
+ if (outcome.status === 'blocked') {
2414
+ return this.publishBlockedTerminal(active, outcome, evidenceCode);
2415
+ }
2416
+ return this.persistTerminal(active, outcome, evidenceCode, retainIntent);
2417
+ }
2418
+ async publishBlockedTerminal(starting, outcome, evidenceCode) {
2419
+ let active = starting;
2420
+ const autoBlocked = [active.config.github.labels.auto.name, active.config.github.labels.blocked.name].sort();
2421
+ const blockedOnly = [active.config.github.labels.blocked.name];
2422
+ let expected = autoBlocked;
2423
+ const existing = active.record.intent;
2424
+ if (!existing) {
2425
+ active = await this.persist(active, { intent: {
2426
+ kind: 'blocked-labels',
2427
+ issueNumber: active.record.issueNumber,
2428
+ expected,
2429
+ blockKind: outcome.kind,
2430
+ resumable: outcome.resumable,
2431
+ evidenceCode,
2432
+ } });
2433
+ }
2434
+ else if (existing.kind !== 'blocked-labels'
2435
+ || existing.issueNumber !== active.record.issueNumber
2436
+ || existing.blockKind !== outcome.kind
2437
+ || existing.resumable !== outcome.resumable
2438
+ || existing.evidenceCode !== evidenceCode) {
2439
+ return this.invokedFailure(active, 'blocked-labels-intent-diverged');
2440
+ }
2441
+ else {
2442
+ expected = existing.expected;
2443
+ if (!sameStrings(expected, autoBlocked) && !sameStrings(expected, blockedOnly) && expected.length !== 0) {
2444
+ return this.invokedFailure(active, 'blocked-labels-intent-diverged');
2445
+ }
2446
+ }
2447
+ let issue = await this.readIssue(active.record.issueNumber);
2448
+ if (!issue)
2449
+ return this.invokedFailure(active, 'blocked-labels-issue-missing');
2450
+ let projection = blockedLabelProjection(issue.labels, active.config);
2451
+ if (issue.state === 'OPEN' && projection.status === 'transition') {
2452
+ if (!this.dependencies.issues.transitionToBlocked) {
2453
+ return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
2454
+ }
2455
+ try {
2456
+ await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
2457
+ }
2458
+ catch {
2459
+ return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
2460
+ }
2461
+ issue = await this.readIssue(active.record.issueNumber);
2462
+ projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
2463
+ }
2464
+ if (!issue)
2465
+ return this.invokedFailure(active, 'blocked-labels-observation-missing');
2466
+ if (issue.state === 'OPEN') {
2467
+ if (projection.status !== 'settled') {
2468
+ return this.invokedFailure(active, 'blocked-labels-observation-diverged');
2469
+ }
2470
+ if (!sameStrings(expected, projection.expected)) {
2471
+ expected = projection.expected;
2472
+ const intent = active.record.intent;
2473
+ if (intent?.kind !== 'blocked-labels')
2474
+ return this.invokedFailure(active, 'blocked-labels-intent-diverged');
2475
+ try {
2476
+ active = await this.persist(active, { intent: { ...intent, expected } });
2477
+ }
2478
+ catch {
2479
+ throw new PostEffectStateError(active);
2480
+ }
2481
+ }
2482
+ }
2483
+ try {
2484
+ return await this.persistTerminal(active, outcome, evidenceCode, false);
2485
+ }
2486
+ catch {
2487
+ throw new PostEffectStateError(active);
2488
+ }
2489
+ }
2490
+ async reconcilePersistedBlockedTerminal(active, issue, outcome) {
2491
+ if (!this.dependencies.issues.transitionToBlocked) {
2492
+ return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
2493
+ }
2494
+ try {
2495
+ await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
2496
+ }
2497
+ catch {
2498
+ return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
2499
+ }
2500
+ const observation = await this.readIssue(active.record.issueNumber);
2501
+ if (!observation || observation.state !== 'OPEN'
2502
+ || blockedLabelProjection(observation.labels, active.config).status !== 'settled') {
2503
+ return this.invokedFailure(active, 'blocked-labels-observation-diverged');
2504
+ }
2505
+ return publicOutcome(outcome);
2506
+ }
2507
+ async persistTerminal(active, outcome, evidenceCode, retainIntent) {
2255
2508
  const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
2256
2509
  const terminalOutcome = { ...outcome, evidencePath: evidence.path };
2257
2510
  const changes = {
@@ -2263,7 +2516,7 @@ export class RunIssue {
2263
2516
  if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
2264
2517
  changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
2265
2518
  ? { status: 'blocked', kind: outcome.kind }
2266
- : { status: outcome.status });
2519
+ : { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
2267
2520
  }
2268
2521
  if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
2269
2522
  if (active.record.waitingHuman.phase === 'resumed') {
@@ -2503,6 +2756,36 @@ function sameStrings(left, right) {
2503
2756
  const b = [...right].sort();
2504
2757
  return a.length === b.length && a.every((value, index) => value === b[index]);
2505
2758
  }
2759
+ function blockedLabelPolicy(config) {
2760
+ return {
2761
+ auto: config.github.labels.auto.name,
2762
+ running: config.github.labels.running.name,
2763
+ blocked: config.github.labels.blocked.name,
2764
+ review: config.github.labels.review.name,
2765
+ waitingHuman: config.github.labels.waitingHuman.name,
2766
+ };
2767
+ }
2768
+ function blockedLabelProjection(labels, config) {
2769
+ const policy = blockedLabelPolicy(config);
2770
+ const present = new Set(labels);
2771
+ if (present.has(policy.review) || present.has(policy.waitingHuman))
2772
+ return { status: 'diverged' };
2773
+ const auto = present.has(policy.auto);
2774
+ const running = present.has(policy.running);
2775
+ const blocked = present.has(policy.blocked);
2776
+ if (running || (auto && !blocked))
2777
+ return { status: 'transition' };
2778
+ if (blocked)
2779
+ return { status: 'settled', expected: auto ? [policy.auto, policy.blocked].sort() : [policy.blocked] };
2780
+ return { status: 'settled', expected: [] };
2781
+ }
2782
+ function sameCheckPolicy(checks, policy) {
2783
+ const expected = Object.entries(policy).sort(([left], [right]) => left.localeCompare(right));
2784
+ const actual = checks.map((check) => [check.id, check.command])
2785
+ .sort(([left], [right]) => left.localeCompare(right));
2786
+ return actual.length === expected.length
2787
+ && actual.every(([id, command], index) => id === expected[index]?.[0] && command === expected[index]?.[1]);
2788
+ }
2506
2789
  function sameFreshness(left, right) {
2507
2790
  return left.headSha === right.headSha
2508
2791
  && left.indexTreeSha === right.indexTreeSha