codex-orchestrator 2.0.4 → 2.0.6

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 (92) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +30 -3
  3. package/dist/src/v2/acceptance-proof.d.ts +12 -0
  4. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  5. package/dist/src/v2/acceptance-proof.js +120 -6
  6. package/dist/src/v2/acceptance-proof.js.map +1 -1
  7. package/dist/src/v2/adapters/command.d.ts +2 -0
  8. package/dist/src/v2/adapters/command.d.ts.map +1 -1
  9. package/dist/src/v2/adapters/command.js +67 -8
  10. package/dist/src/v2/adapters/command.js.map +1 -1
  11. package/dist/src/v2/adapters/gh-issue-adapter.js +16 -5
  12. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  13. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts +7 -1
  14. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts.map +1 -1
  15. package/dist/src/v2/adapters/gh-pull-request-adapter.js +288 -0
  16. package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
  17. package/dist/src/v2/adapters/pull-requests.d.ts +69 -0
  18. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  19. package/dist/src/v2/adapters/pull-requests.js +48 -0
  20. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  21. package/dist/src/v2/adapters/worktree.d.ts +1 -0
  22. package/dist/src/v2/adapters/worktree.d.ts.map +1 -1
  23. package/dist/src/v2/adapters/worktree.js +10 -1
  24. package/dist/src/v2/adapters/worktree.js.map +1 -1
  25. package/dist/src/v2/android-proof-runner.d.ts +93 -0
  26. package/dist/src/v2/android-proof-runner.d.ts.map +1 -0
  27. package/dist/src/v2/android-proof-runner.js +921 -0
  28. package/dist/src/v2/android-proof-runner.js.map +1 -0
  29. package/dist/src/v2/cli.d.ts +9 -0
  30. package/dist/src/v2/cli.d.ts.map +1 -1
  31. package/dist/src/v2/cli.js +38 -11
  32. package/dist/src/v2/cli.js.map +1 -1
  33. package/dist/src/v2/config.d.ts +13 -1
  34. package/dist/src/v2/config.d.ts.map +1 -1
  35. package/dist/src/v2/config.js +57 -4
  36. package/dist/src/v2/config.js.map +1 -1
  37. package/dist/src/v2/containment.d.ts +11 -2
  38. package/dist/src/v2/containment.d.ts.map +1 -1
  39. package/dist/src/v2/containment.js +35 -7
  40. package/dist/src/v2/containment.js.map +1 -1
  41. package/dist/src/v2/direct-delivery.d.ts +1 -1
  42. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  43. package/dist/src/v2/direct-delivery.js +9 -3
  44. package/dist/src/v2/direct-delivery.js.map +1 -1
  45. package/dist/src/v2/immutable-workflow-publisher.js +6 -1
  46. package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
  47. package/dist/src/v2/mobile-lease.d.ts +9 -0
  48. package/dist/src/v2/mobile-lease.d.ts.map +1 -1
  49. package/dist/src/v2/mobile-lease.js +27 -3
  50. package/dist/src/v2/mobile-lease.js.map +1 -1
  51. package/dist/src/v2/review-feedback-coordinator.d.ts +54 -0
  52. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -0
  53. package/dist/src/v2/review-feedback-coordinator.js +245 -0
  54. package/dist/src/v2/review-feedback-coordinator.js.map +1 -0
  55. package/dist/src/v2/review-feedback.d.ts +127 -0
  56. package/dist/src/v2/review-feedback.d.ts.map +1 -0
  57. package/dist/src/v2/review-feedback.js +436 -0
  58. package/dist/src/v2/review-feedback.js.map +1 -0
  59. package/dist/src/v2/run-issue.d.ts +61 -0
  60. package/dist/src/v2/run-issue.d.ts.map +1 -1
  61. package/dist/src/v2/run-issue.js +843 -66
  62. package/dist/src/v2/run-issue.js.map +1 -1
  63. package/dist/src/v2/run-store.d.ts +48 -1
  64. package/dist/src/v2/run-store.d.ts.map +1 -1
  65. package/dist/src/v2/run-store.js +125 -5
  66. package/dist/src/v2/run-store.js.map +1 -1
  67. package/dist/src/v2/runtime.d.ts +37 -0
  68. package/dist/src/v2/runtime.d.ts.map +1 -1
  69. package/dist/src/v2/runtime.js +183 -15
  70. package/dist/src/v2/runtime.js.map +1 -1
  71. package/dist/src/v2/setup.js +1 -1
  72. package/dist/src/v2/setup.js.map +1 -1
  73. package/docs/deep-dive.md +71 -7
  74. package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
  75. package/internal-workflow/evals/coding-skill-evals.json +18 -0
  76. package/internal-workflow/manifest.json +1 -1
  77. package/internal-workflow/skills/acceptance-proof/SKILL.md +1 -1
  78. package/internal-workflow/skills/acceptance-proof/references/android.md +6 -6
  79. package/internal-workflow/skills/code-debugger/SKILL.md +3 -3
  80. package/internal-workflow/skills/code-review/SKILL.md +18 -6
  81. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +10 -8
  82. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
  83. package/internal-workflow/skills/implementation-spec-review/SKILL.md +17 -3
  84. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +30 -0
  85. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +30 -6
  86. package/internal-workflow/skills/spec-implementer/references/review-loop.md +7 -1
  87. package/internal-workflow/skills/tdd/SKILL.md +5 -4
  88. package/internal-workflow/skills/tdd/evals/evals.json +18 -0
  89. package/internal-workflow/skills/tdd/mocking.md +3 -42
  90. package/internal-workflow/skills/tdd/refactoring.md +6 -8
  91. package/package.json +1 -1
  92. package/internal-workflow/skills/acceptance-proof/tools/android-lease.mjs +0 -280
@@ -1,12 +1,14 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { spawn } from 'node:child_process';
4
- import { lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
4
+ import { access, lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
5
5
  import { homedir, hostname } from 'node:os';
6
6
  import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
7
7
  import { writeDurableAtomicFile } from './adapters/durable-atomic-file.js';
8
8
  import { GitWorktreeManager } from './adapters/worktree.js';
9
+ import { ReviewFeedbackCoordinator } from './review-feedback-coordinator.js';
9
10
  import { defaultProcessExecutor } from './adapters/command.js';
11
+ import { RunnerAndroidProofController } from './android-proof-runner.js';
10
12
  import { AcceptanceProof, ProofQuiescenceError } from './acceptance-proof.js';
11
13
  import { createCheckedChangeCapabilities } from './checked-change.js';
12
14
  import { InjectedContainedReportOperation } from './contained-report-operation.js';
@@ -47,6 +49,30 @@ export class LocalGitRunIssueAdapter {
47
49
  requiredBaseSha: input.baseSha,
48
50
  });
49
51
  }
52
+ async ensureContinuationWorktree(input) {
53
+ const localHead = (await this.git([
54
+ '-C', input.targetRoot, 'rev-parse', '--verify', `refs/heads/${input.branchName}`,
55
+ ])).trim();
56
+ const remoteHead = await this.getRemoteBranchSha(input.targetRoot, input.branchName);
57
+ if (localHead !== input.publishedHeadSha || remoteHead !== input.publishedHeadSha) {
58
+ throw new Error('continuation branch refs do not exactly match the published head');
59
+ }
60
+ await this.worktrees.ensureIssueWorktree({
61
+ targetRoot: input.targetRoot,
62
+ workspacePath: input.worktreePath,
63
+ branchName: input.branchName,
64
+ baseBranch: input.baseBranch,
65
+ requiredBaseSha: input.publishedHeadSha,
66
+ allowResume: true,
67
+ });
68
+ const observed = await this.inspectWorktree({
69
+ worktreePath: input.worktreePath,
70
+ branchName: input.branchName,
71
+ baseSha: input.publishedHeadSha,
72
+ });
73
+ if (observed !== 'matching')
74
+ throw new Error('continuation worktree does not match the published head');
75
+ }
50
76
  async inspectWorktree(input) {
51
77
  try {
52
78
  const stat = await lstat(input.worktreePath);
@@ -110,6 +136,9 @@ export class LocalGitRunIssueAdapter {
110
136
  listChangedFiles(worktreePath) {
111
137
  return this.worktrees.listChangedFiles(worktreePath);
112
138
  }
139
+ listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot) {
140
+ return this.worktrees.listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot);
141
+ }
113
142
  async fingerprintChangedFiles(worktreePath, changedFiles) {
114
143
  const root = await realpath(worktreePath);
115
144
  const entries = [];
@@ -193,6 +222,12 @@ export class ContainedImplementationAgent {
193
222
  bootId: this.dependencies.bootId,
194
223
  });
195
224
  const baseline = await this.dependencies.git.snapshot(input.worktreePath);
225
+ await input.onPrepared?.({
226
+ attemptId,
227
+ reportPath: attempt.reportPath,
228
+ preparedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
229
+ baseline,
230
+ });
196
231
  try {
197
232
  const result = await (this.dependencies.process ?? new CodexProcess()).run({
198
233
  codexPath: config.codex.command,
@@ -210,6 +245,8 @@ export class ContainedImplementationAgent {
210
245
  `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
211
246
  `Implement issue #${input.issue.number}: ${input.issue.title}`,
212
247
  `Implementation cycle: ${input.cycle}.`,
248
+ ...(input.reviewFeedbackRound ? [`Pull-request feedback repair round: ${input.reviewFeedbackRound}.`] : []),
249
+ ...(input.reviewFeedback?.length ? [`Frozen trusted pull-request feedback: ${canonicalJson(input.reviewFeedback)}`] : []),
213
250
  `Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
214
251
  ...(input.reworkFindings.length > 0 ? [`Repair these verified findings: ${canonicalJson(input.reworkFindings)}`] : []),
215
252
  ...(input.repairOnly ? ['Report repair only: do not modify any worktree file; emit a schema-valid implementation report for the existing change.'] : []),
@@ -219,6 +256,9 @@ export class ContainedImplementationAgent {
219
256
  idleTimeoutMs: config.codex.idleTimeoutMs,
220
257
  operationPolicy: attempt.policy,
221
258
  executionProfile: attempt.profile,
259
+ ...(input.onLaunched ? { onSpawned: ({ pid, processGroupId }) => input.onLaunched({
260
+ attemptId, pid, processGroupId, launchedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
261
+ }) } : {}),
222
262
  }, input.signal);
223
263
  if (result.kind === 'cancelled')
224
264
  return { kind: 'cancelled' };
@@ -248,6 +288,26 @@ export class ContainedImplementationAgent {
248
288
  };
249
289
  }
250
290
  }
291
+ async recover(input) {
292
+ const expected = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(input.canonicalRepository), 'runs', input.runId, 'attempts', input.invocation.attemptId, 'report.json');
293
+ if (resolve(input.invocation.reportPath) !== expected) {
294
+ throw new Error('review feedback implementation report path is not attempt-owned');
295
+ }
296
+ let bytes;
297
+ try {
298
+ bytes = await readRegularFile(expected);
299
+ }
300
+ catch (error) {
301
+ if (isErrorCode(error, 'ENOENT'))
302
+ return undefined;
303
+ throw error;
304
+ }
305
+ return {
306
+ kind: 'completed',
307
+ attemptId: input.invocation.attemptId,
308
+ report: decodeAgentReportForValidation(bytes),
309
+ };
310
+ }
251
311
  }
252
312
  export class ContainedProofAgent {
253
313
  dependencies;
@@ -271,8 +331,6 @@ export class ContainedProofAgent {
271
331
  });
272
332
  const artifactRoot = resolve(worktreePath, config.proof.artifactDir);
273
333
  const snapshotRoot = dirname(attempt.sourceSkillPath ?? attempt.operationPath);
274
- const androidLeaseRoot = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(canonicalRepository), 'leases');
275
- const androidLeaseArtifact = join(artifactRoot, input.proofId, 'android-lease.json');
276
334
  const iosLeaseRoot = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(canonicalRepository), 'leases');
277
335
  const iosLeaseArtifact = join(artifactRoot, input.proofId, 'ios-lease.json');
278
336
  const iosTooling = await discoverIosTooling(this.dependencies.processExecutor, this.dependencies.iosXcrunPath);
@@ -300,12 +358,14 @@ export class ContainedProofAgent {
300
358
  `Write evidence only below ${config.proof.artifactDir}.`,
301
359
  'When a frozen criterion has a browser surface, follow references/browser.md from the exact acceptance-proof skill snapshot.',
302
360
  'When a frozen criterion has an Android surface, follow references/android.md from the exact acceptance-proof skill snapshot.',
303
- `Android lease helper: ${join(snapshotRoot, 'tools', 'android-lease.mjs')}.`,
304
- `Android lease root: ${androidLeaseRoot}.`,
305
- `Android lease artifact: ${androidLeaseArtifact}.`,
306
- `Android lease proof ID: ${input.proofId}.`,
307
- `Android lease owner PID: ${process.pid}.`,
308
- `Android adb path: ${this.dependencies.androidAdbPath}.`,
361
+ ...(config.proof.android ? [
362
+ `Runner-owned Android artifact paths: ${canonicalJson(input.runnerPreparedArtifactPaths)}.`,
363
+ `Runner-owned Android preparation warnings: ${canonicalJson(input.runnerPreparationWarnings)}.`,
364
+ 'The trusted Runner owns emulator, build, adb, lease, capture, and cleanup actions. Do not invoke adb, emulator, Flutter run, or an Android lease helper.',
365
+ 'Inspect Runner artifacts when present. If preparation warnings are present, continue with all available non-visual evidence and preserve the warning as a residual risk; Android infrastructure failure alone must not block delivery.',
366
+ ] : [
367
+ 'Runner-owned Android proof is not configured for this repository. Do not invoke adb, emulator, Flutter run, or an Android lease helper; return a typed tool blocker for Android criteria.',
368
+ ]),
309
369
  'When a frozen criterion has an iOS surface, follow references/ios.md from the exact acceptance-proof skill snapshot.',
310
370
  `iOS lease helper: ${join(snapshotRoot, 'tools', 'ios-lease.mjs')}.`,
311
371
  `iOS lease root: ${iosLeaseRoot}.`,
@@ -347,11 +407,43 @@ export class ContainedProofAgent {
347
407
  }
348
408
  }
349
409
  }
410
+ export async function resolveCodexExecutable(command, safePath) {
411
+ if (command.length === 0 || safePath.length === 0)
412
+ throw new Error('Codex command and safe path must be non-empty');
413
+ if (!isAbsolute(command) && command.includes('/'))
414
+ throw new Error('relative Codex command paths are not supported');
415
+ const candidates = isAbsolute(command)
416
+ ? [command]
417
+ : safePath.split(':').filter((entry) => entry.length > 0).map((entry) => join(entry, command));
418
+ for (const candidate of candidates) {
419
+ try {
420
+ const canonical = await realpath(candidate);
421
+ const stat = await lstat(canonical);
422
+ if (!stat.isFile())
423
+ continue;
424
+ await access(canonical, constants.X_OK);
425
+ return canonical;
426
+ }
427
+ catch {
428
+ // Try the next safe-path entry.
429
+ }
430
+ }
431
+ throw new Error('Codex executable is unavailable in the configured safe path');
432
+ }
350
433
  export function createV2Runtime(input) {
351
434
  const targetRoot = resolve(input.targetRoot);
352
435
  const orchestratorHome = resolve(input.orchestratorHome);
353
436
  const now = input.now ?? (() => new Date().toISOString());
354
437
  const commandExecutor = input.processExecutor ?? defaultProcessExecutor;
438
+ const runtimeSafePath = requireRuntimeString(input.safePath, 'safePath');
439
+ let codexExecutable;
440
+ const resolveRuntimeCodex = async (command) => {
441
+ if (codexExecutable?.command === command)
442
+ return codexExecutable.path;
443
+ const path = await resolveCodexExecutable(command, runtimeSafePath);
444
+ codexExecutable = { command, path };
445
+ return path;
446
+ };
355
447
  const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor);
356
448
  const proofFreshnessGit = git;
357
449
  const controller = new AbortController();
@@ -364,12 +456,13 @@ export function createV2Runtime(input) {
364
456
  ?? (process.env.ANDROID_SDK_ROOT ? join(process.env.ANDROID_SDK_ROOT, 'platform-tools', 'adb') : undefined)
365
457
  ?? join(homedir(), 'Library', 'Android', 'sdk', 'platform-tools', 'adb');
366
458
  const androidAdbPath = resolve(configuredAndroidAdbPath);
459
+ const androidEmulatorPath = join(dirname(dirname(androidAdbPath)), 'emulator', 'emulator');
367
460
  const iosXcrunPath = resolve(input.iosXcrunPath ?? '/usr/bin/xcrun');
368
461
  const containedDependencies = () => ({
369
462
  config: () => requireConfig(currentConfig),
370
463
  orchestratorHome,
371
464
  parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
372
- safePath: requireRuntimeString(input.safePath, 'safePath'),
465
+ safePath: runtimeSafePath,
373
466
  bootId: input.bootId,
374
467
  process: containedProcess,
375
468
  createAttemptId: input.createAttemptId,
@@ -386,6 +479,12 @@ export function createV2Runtime(input) {
386
479
  iosXcrunPath,
387
480
  processExecutor: commandExecutor,
388
481
  });
482
+ const androidProofController = new RunnerAndroidProofController({
483
+ adbPath: androidAdbPath,
484
+ emulatorPath: androidEmulatorPath,
485
+ execute: commandExecutor,
486
+ now: () => new Date(now()),
487
+ });
389
488
  const reportOperation = new InjectedContainedReportOperation({
390
489
  prepare: async ({ operation, attemptId, runId, workflowGeneration }) => ({
391
490
  operation,
@@ -676,7 +775,11 @@ export function createV2Runtime(input) {
676
775
  if (resolve(requestedRoot) !== targetRoot)
677
776
  throw new Error('runtime target root mismatch');
678
777
  const bytes = await readRegularFile(join(targetRoot, '.codex-orchestrator', 'config.json'));
679
- const config = parseAgentAutoConfig(parseJsonWithoutDuplicateKeys(bytes.toString('utf8')));
778
+ const parsed = parseAgentAutoConfig(parseJsonWithoutDuplicateKeys(bytes.toString('utf8')));
779
+ const config = {
780
+ ...parsed,
781
+ codex: { ...parsed.codex, command: await resolveRuntimeCodex(parsed.codex.command) },
782
+ };
680
783
  const path = join(targetRoot, config.runner.stateDir, 'v2', 'run-state.json');
681
784
  if (!runRecords)
682
785
  runRecords = new FileRunRecordWriter(path);
@@ -708,6 +811,7 @@ export function createV2Runtime(input) {
708
811
  worktreeRoot: worktreePath,
709
812
  now: () => new Date(now()),
710
813
  artifactRelativePathForProof: (proofId) => `${config.proof.artifactDir}/${proofId}/android-lease.json`,
814
+ targetController: androidProofController,
711
815
  });
712
816
  const iosLease = new FileIosLeaseVerifier({
713
817
  leaseRoot: join(orchestratorHome, 'v2', repoKey, 'leases'),
@@ -737,18 +841,56 @@ export function createV2Runtime(input) {
737
841
  now,
738
842
  signal: controller.signal,
739
843
  });
740
- return acceptanceProof.proveChange(proofInput);
844
+ const androidRelevant = !!config.proof.android && isAndroidProofRelevant(proofInput.issue, proofInput.frozenCriteria, checked.payload.changedFiles);
845
+ const runnerPreparedArtifactPaths = [];
846
+ const runnerPreparedArtifactSha256 = {};
847
+ const runnerPreparationWarnings = [];
848
+ let preparationAttempted = false;
849
+ return acceptanceProof.proveChange({
850
+ ...proofInput,
851
+ runnerPreparedArtifactPaths,
852
+ runnerPreparedArtifactSha256,
853
+ runnerPreparationWarnings,
854
+ beforeAgentLaunch: async () => {
855
+ await proofInput.beforeAgentLaunch?.();
856
+ if (!androidRelevant || preparationAttempted)
857
+ return;
858
+ preparationAttempted = true;
859
+ const prepared = await androidProofController.prepare({
860
+ proofId: proofInput.proofId,
861
+ worktreePath,
862
+ artifactDir: config.proof.artifactDir,
863
+ leaseRoot: join(orchestratorHome, 'v2', repoKey, 'leases'),
864
+ config: config.proof.android,
865
+ checks: checked.payload.checks,
866
+ checkedChangeSha256: checked.checkedChangeSha256,
867
+ proofAgentBudgetMs: config.codex.timeoutMs * 3,
868
+ signal: controller.signal,
869
+ });
870
+ if (prepared.status === 'prepared') {
871
+ runnerPreparedArtifactPaths.splice(0, runnerPreparedArtifactPaths.length, ...prepared.runnerPreparedArtifactPaths);
872
+ Object.assign(runnerPreparedArtifactSha256, prepared.runnerPreparedArtifactSha256);
873
+ }
874
+ else {
875
+ runnerPreparationWarnings.push(`Android UI proof unfinished: ${prepared.summary}`);
876
+ }
877
+ },
878
+ });
741
879
  },
742
880
  };
743
881
  const runner = new RunIssue({
744
882
  readConfig,
745
883
  validateContainment: async (config) => {
746
884
  const certificate = await readContainmentCertificate(containmentCertificatePath(orchestratorHome));
747
- const version = await commandExecutor(config.codex.command, ['--version']);
885
+ const executablePath = await resolveRuntimeCodex(config.codex.command);
886
+ const version = await commandExecutor(executablePath, ['--version']);
748
887
  if (version.exitCode !== 0)
749
888
  throw new Error('Codex version is unavailable');
750
889
  assertContainmentCertificateMatchesRuntime(certificate, {
751
890
  codexVersion: version.stdout.trim(),
891
+ codexExecutablePath: executablePath,
892
+ codexExecutableSha256: sha256(await readFile(executablePath)),
893
+ packageVersion: input.packageVersion,
752
894
  argvPolicySha256: containmentArgvPolicySha256(),
753
895
  });
754
896
  },
@@ -777,7 +919,13 @@ export function createV2Runtime(input) {
777
919
  url: issue.url,
778
920
  state: 'OPEN',
779
921
  labels: issue.labels.map((label) => label.name).sort(),
780
- comments: comments.map((comment) => ({ body: comment.body, authorAssociation: comment.authorAssociation })),
922
+ comments: comments.map((comment) => ({
923
+ id: comment.id,
924
+ body: comment.body,
925
+ authorAssociation: comment.authorAssociation,
926
+ createdAt: comment.createdAt,
927
+ updatedAt: comment.updatedAt,
928
+ })),
781
929
  };
782
930
  },
783
931
  setLabels: async (issueNumber, labels) => {
@@ -796,10 +944,26 @@ export function createV2Runtime(input) {
796
944
  if (matches.length > 1)
797
945
  throw new Error('multiple open pull requests match publication intent');
798
946
  const match = matches[0];
799
- return match ? { url: match.url, body: match.body } : undefined;
947
+ if (!match)
948
+ return undefined;
949
+ const reviewTarget = await input.pullRequests.getReviewTarget(match.number);
950
+ return {
951
+ url: match.url,
952
+ body: match.body,
953
+ number: match.number,
954
+ nodeId: match.nodeId,
955
+ ...(reviewTarget ? { headSha: reviewTarget.headRefOid } : {}),
956
+ };
800
957
  },
801
958
  createDraft: async ({ title, body, headBranch, baseBranch }) => input.pullRequests.createDraftPullRequest({ title, body, headBranch, baseBranch }),
959
+ listConversationComments: async (number) => (await input.pullRequests.listConversationComments(number))
960
+ .map((comment) => ({ id: comment.id, body: comment.body })),
961
+ postConversationComment: async (number, body) => {
962
+ const comment = await input.pullRequests.postConversationComment(number, body);
963
+ return { id: comment.id, body: comment.body };
964
+ },
802
965
  },
966
+ reviewFeedback: new ReviewFeedbackCoordinator({ pullRequests: input.pullRequests, issues: input.issues, now }),
803
967
  git,
804
968
  routeCoordinator: {
805
969
  run: ({ state, ...routeInput }) => new RouteCoordinator({
@@ -1332,6 +1496,10 @@ async function scrubReportReadView(directory) {
1332
1496
  }
1333
1497
  }
1334
1498
  }
1499
+ function isAndroidProofRelevant(issue, criteria, changedFiles) {
1500
+ return /\bandroid\b/iu.test([issue.title, issue.body, ...criteria.map((criterion) => criterion.text)].join('\n'))
1501
+ || changedFiles.some((path) => /^(?:android\/|lib\/|assets\/|fonts\/|pubspec\.(?:yaml|lock)$)/u.test(path));
1502
+ }
1335
1503
  function isErrorCode(error, code) {
1336
1504
  return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
1337
1505
  }