codex-orchestrator 2.0.3 → 2.0.5

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 (161) hide show
  1. package/CHANGELOG.md +51 -427
  2. package/README.md +161 -37
  3. package/dist/src/index.d.ts +1 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/v2/acceptance-proof.d.ts +5 -0
  6. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  7. package/dist/src/v2/acceptance-proof.js +10 -2
  8. package/dist/src/v2/acceptance-proof.js.map +1 -1
  9. package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
  10. package/dist/src/v2/adapters/gh-issue-adapter.js +6 -7
  11. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  12. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts +7 -1
  13. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts.map +1 -1
  14. package/dist/src/v2/adapters/gh-pull-request-adapter.js +288 -0
  15. package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
  16. package/dist/src/v2/adapters/pull-requests.d.ts +69 -0
  17. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  18. package/dist/src/v2/adapters/pull-requests.js +48 -0
  19. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  20. package/dist/src/v2/adapters/worktree.d.ts +1 -0
  21. package/dist/src/v2/adapters/worktree.d.ts.map +1 -1
  22. package/dist/src/v2/adapters/worktree.js +10 -1
  23. package/dist/src/v2/adapters/worktree.js.map +1 -1
  24. package/dist/src/v2/cli-contract.d.ts +3 -3
  25. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  26. package/dist/src/v2/cli-contract.js +1 -3
  27. package/dist/src/v2/cli-contract.js.map +1 -1
  28. package/dist/src/v2/cli.d.ts +33 -0
  29. package/dist/src/v2/cli.d.ts.map +1 -0
  30. package/dist/src/v2/{candidate-cli.js → cli.js} +55 -39
  31. package/dist/src/v2/cli.js.map +1 -0
  32. package/dist/src/v2/code-review-report.d.ts +1 -1
  33. package/dist/src/v2/code-review-report.d.ts.map +1 -1
  34. package/dist/src/v2/code-review-report.js +2 -2
  35. package/dist/src/v2/code-review-report.js.map +1 -1
  36. package/dist/src/v2/codex-process.d.ts.map +1 -1
  37. package/dist/src/v2/codex-process.js +12 -1
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +2 -3
  40. package/dist/src/v2/config.d.ts.map +1 -1
  41. package/dist/src/v2/config.js +0 -3
  42. package/dist/src/v2/config.js.map +1 -1
  43. package/dist/src/v2/contained-report-operation.d.ts +2 -2
  44. package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
  45. package/dist/src/v2/contained-report-operation.js +1 -1
  46. package/dist/src/v2/contained-report-operation.js.map +1 -1
  47. package/dist/src/v2/containment.d.ts +15 -2
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +43 -6
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +5 -10
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  53. package/dist/src/v2/direct-delivery.js +32 -91
  54. package/dist/src/v2/direct-delivery.js.map +1 -1
  55. package/dist/src/v2/proof-report.d.ts.map +1 -1
  56. package/dist/src/v2/proof-report.js +55 -29
  57. package/dist/src/v2/proof-report.js.map +1 -1
  58. package/dist/src/v2/review-feedback-coordinator.d.ts +54 -0
  59. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -0
  60. package/dist/src/v2/review-feedback-coordinator.js +245 -0
  61. package/dist/src/v2/review-feedback-coordinator.js.map +1 -0
  62. package/dist/src/v2/review-feedback.d.ts +127 -0
  63. package/dist/src/v2/review-feedback.d.ts.map +1 -0
  64. package/dist/src/v2/review-feedback.js +436 -0
  65. package/dist/src/v2/review-feedback.js.map +1 -0
  66. package/dist/src/v2/run-issue.d.ts +63 -9
  67. package/dist/src/v2/run-issue.d.ts.map +1 -1
  68. package/dist/src/v2/run-issue.js +798 -78
  69. package/dist/src/v2/run-issue.js.map +1 -1
  70. package/dist/src/v2/run-store.d.ts +49 -5
  71. package/dist/src/v2/run-store.d.ts.map +1 -1
  72. package/dist/src/v2/run-store.js +138 -44
  73. package/dist/src/v2/run-store.js.map +1 -1
  74. package/dist/src/v2/runtime.d.ts +40 -3
  75. package/dist/src/v2/runtime.d.ts.map +1 -1
  76. package/dist/src/v2/runtime.js +245 -52
  77. package/dist/src/v2/runtime.js.map +1 -1
  78. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  79. package/dist/src/v2/setup-cli.js +4 -11
  80. package/dist/src/v2/setup-cli.js.map +1 -1
  81. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  82. package/dist/src/v2/setup-runtime.js +1 -61
  83. package/dist/src/v2/setup-runtime.js.map +1 -1
  84. package/dist/src/v2/setup-store.d.ts +0 -5
  85. package/dist/src/v2/setup-store.d.ts.map +1 -1
  86. package/dist/src/v2/setup-store.js +3 -106
  87. package/dist/src/v2/setup-store.js.map +1 -1
  88. package/dist/src/v2/setup.d.ts +6 -46
  89. package/dist/src/v2/setup.d.ts.map +1 -1
  90. package/dist/src/v2/setup.js +12 -294
  91. package/dist/src/v2/setup.js.map +1 -1
  92. package/dist/src/v2/workflow-assets.d.ts +19 -11
  93. package/dist/src/v2/workflow-assets.d.ts.map +1 -1
  94. package/dist/src/v2/workflow-assets.js +132 -40
  95. package/dist/src/v2/workflow-assets.js.map +1 -1
  96. package/docs/deep-dive.md +328 -56
  97. package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
  98. package/internal-workflow/docs/agents/coding-skill-routing.md +116 -196
  99. package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
  100. package/internal-workflow/docs/agents/review-gates.md +32 -39
  101. package/internal-workflow/docs/agents/review-protocol.md +75 -147
  102. package/internal-workflow/evals/coding-skill-evals.json +84 -0
  103. package/internal-workflow/manifest.json +1 -1
  104. package/internal-workflow/operations/acceptance-proof/SKILL.md +7 -1
  105. package/internal-workflow/operations/ambiguity-review/SKILL.md +2 -0
  106. package/internal-workflow/operations/code-review/SKILL.md +21 -1
  107. package/internal-workflow/operations/implementation/SKILL.md +22 -1
  108. package/internal-workflow/operations/spec-author/SKILL.md +10 -1
  109. package/internal-workflow/operations/spec-review/SKILL.md +10 -1
  110. package/internal-workflow/operations/triage/SKILL.md +10 -1
  111. package/internal-workflow/schemas/code-review-v1.json +1 -1
  112. package/internal-workflow/schemas/proof-report-v1.json +1 -1
  113. package/internal-workflow/skills/agent-auto/SKILL.md +6 -1
  114. package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
  115. package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
  116. package/internal-workflow/skills/code-review/SKILL.md +51 -17
  117. package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
  118. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +15 -6
  119. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
  120. package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +2 -2
  121. package/internal-workflow/skills/implementation-spec-review/SKILL.md +108 -204
  122. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
  123. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
  124. package/internal-workflow/skills/small-task-implementer/SKILL.md +15 -8
  125. package/internal-workflow/skills/spec-implementer/SKILL.md +101 -172
  126. package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
  127. package/internal-workflow/skills/spec-implementer/references/review-loop.md +100 -0
  128. package/internal-workflow/skills/tdd/SKILL.md +20 -6
  129. package/internal-workflow/skills/tdd/agents/openai.yaml +2 -2
  130. package/internal-workflow/skills/tdd/evals/evals.json +18 -0
  131. package/internal-workflow/skills/tdd/mocking.md +3 -42
  132. package/internal-workflow/skills/tdd/refactoring.md +6 -8
  133. package/package.json +9 -6
  134. package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
  135. package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
  136. package/dist/src/v2/adapters/target-activity-fence.js +0 -249
  137. package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
  138. package/dist/src/v2/candidate-cli.d.ts +0 -26
  139. package/dist/src/v2/candidate-cli.d.ts.map +0 -1
  140. package/dist/src/v2/candidate-cli.js.map +0 -1
  141. package/dist/src/v2/legacy-cutover.d.ts +0 -52
  142. package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
  143. package/dist/src/v2/legacy-cutover.js +0 -87
  144. package/dist/src/v2/legacy-cutover.js.map +0 -1
  145. package/internal-workflow/docs/agents/artifact-review-loop.md +0 -267
  146. package/internal-workflow/docs/agents/implementation-review-loop.md +0 -302
  147. package/internal-workflow/operations/cleanup-review/SKILL.md +0 -3
  148. package/internal-workflow/operations/spec-implementation/SKILL.md +0 -3
  149. package/internal-workflow/profiles/implementer_deep.toml +0 -9
  150. package/internal-workflow/profiles/researcher_standard.toml +0 -9
  151. package/internal-workflow/profiles/reviewer_fast.toml +0 -9
  152. package/internal-workflow/skills/cleanup-review/SKILL.md +0 -84
  153. package/internal-workflow/skills/cleanup-review/agents/openai.yaml +0 -6
  154. package/internal-workflow/skills/codebase-design/DEEPENING.md +0 -35
  155. package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +0 -50
  156. package/internal-workflow/skills/codebase-design/SKILL.md +0 -82
  157. package/internal-workflow/skills/codebase-design/agents/openai.yaml +0 -6
  158. package/internal-workflow/skills/research/SKILL.md +0 -107
  159. package/internal-workflow/skills/research/agents/openai.yaml +0 -6
  160. package/internal-workflow/skills/ui-evidence-proof/SKILL.md +0 -123
  161. package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +0 -6
@@ -1,11 +1,12 @@
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
- import { dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
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';
10
11
  import { AcceptanceProof, ProofQuiescenceError } from './acceptance-proof.js';
11
12
  import { createCheckedChangeCapabilities } from './checked-change.js';
@@ -13,7 +14,7 @@ import { InjectedContainedReportOperation } from './contained-report-operation.j
13
14
  import { ContainedImplementationReviewer } from './implementation-reviewer.js';
14
15
  import { parseAgentAutoConfig } from './config.js';
15
16
  import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
16
- import { canonicalJson, containmentArgvPolicySha256, containmentCertificatePath, parseJsonWithoutDuplicateKeys, readContainmentCertificate, sha256, } from './containment.js';
17
+ import { assertContainmentCertificateMatchesRuntime, canonicalJson, containmentArgvPolicySha256, containmentCertificatePath, parseJsonWithoutDuplicateKeys, readContainmentCertificate, sha256, } from './containment.js';
17
18
  import { FileProofRecordWriter } from './proof-store.js';
18
19
  import { acquireOwnerControlLock, OwnerControlLockBlockedError } from './owner-control-lock.js';
19
20
  import { decodeAgentReportForValidation } from './report-envelope.js';
@@ -47,6 +48,30 @@ export class LocalGitRunIssueAdapter {
47
48
  requiredBaseSha: input.baseSha,
48
49
  });
49
50
  }
51
+ async ensureContinuationWorktree(input) {
52
+ const localHead = (await this.git([
53
+ '-C', input.targetRoot, 'rev-parse', '--verify', `refs/heads/${input.branchName}`,
54
+ ])).trim();
55
+ const remoteHead = await this.getRemoteBranchSha(input.targetRoot, input.branchName);
56
+ if (localHead !== input.publishedHeadSha || remoteHead !== input.publishedHeadSha) {
57
+ throw new Error('continuation branch refs do not exactly match the published head');
58
+ }
59
+ await this.worktrees.ensureIssueWorktree({
60
+ targetRoot: input.targetRoot,
61
+ workspacePath: input.worktreePath,
62
+ branchName: input.branchName,
63
+ baseBranch: input.baseBranch,
64
+ requiredBaseSha: input.publishedHeadSha,
65
+ allowResume: true,
66
+ });
67
+ const observed = await this.inspectWorktree({
68
+ worktreePath: input.worktreePath,
69
+ branchName: input.branchName,
70
+ baseSha: input.publishedHeadSha,
71
+ });
72
+ if (observed !== 'matching')
73
+ throw new Error('continuation worktree does not match the published head');
74
+ }
50
75
  async inspectWorktree(input) {
51
76
  try {
52
77
  const stat = await lstat(input.worktreePath);
@@ -110,6 +135,9 @@ export class LocalGitRunIssueAdapter {
110
135
  listChangedFiles(worktreePath) {
111
136
  return this.worktrees.listChangedFiles(worktreePath);
112
137
  }
138
+ listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot) {
139
+ return this.worktrees.listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot);
140
+ }
113
141
  async fingerprintChangedFiles(worktreePath, changedFiles) {
114
142
  const root = await realpath(worktreePath);
115
143
  const entries = [];
@@ -193,6 +221,12 @@ export class ContainedImplementationAgent {
193
221
  bootId: this.dependencies.bootId,
194
222
  });
195
223
  const baseline = await this.dependencies.git.snapshot(input.worktreePath);
224
+ await input.onPrepared?.({
225
+ attemptId,
226
+ reportPath: attempt.reportPath,
227
+ preparedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
228
+ baseline,
229
+ });
196
230
  try {
197
231
  const result = await (this.dependencies.process ?? new CodexProcess()).run({
198
232
  codexPath: config.codex.command,
@@ -210,6 +244,8 @@ export class ContainedImplementationAgent {
210
244
  `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
211
245
  `Implement issue #${input.issue.number}: ${input.issue.title}`,
212
246
  `Implementation cycle: ${input.cycle}.`,
247
+ ...(input.reviewFeedbackRound ? [`Pull-request feedback repair round: ${input.reviewFeedbackRound}.`] : []),
248
+ ...(input.reviewFeedback?.length ? [`Frozen trusted pull-request feedback: ${canonicalJson(input.reviewFeedback)}`] : []),
213
249
  `Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
214
250
  ...(input.reworkFindings.length > 0 ? [`Repair these verified findings: ${canonicalJson(input.reworkFindings)}`] : []),
215
251
  ...(input.repairOnly ? ['Report repair only: do not modify any worktree file; emit a schema-valid implementation report for the existing change.'] : []),
@@ -219,6 +255,9 @@ export class ContainedImplementationAgent {
219
255
  idleTimeoutMs: config.codex.idleTimeoutMs,
220
256
  operationPolicy: attempt.policy,
221
257
  executionProfile: attempt.profile,
258
+ ...(input.onLaunched ? { onSpawned: ({ pid, processGroupId }) => input.onLaunched({
259
+ attemptId, pid, processGroupId, launchedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
260
+ }) } : {}),
222
261
  }, input.signal);
223
262
  if (result.kind === 'cancelled')
224
263
  return { kind: 'cancelled' };
@@ -248,6 +287,26 @@ export class ContainedImplementationAgent {
248
287
  };
249
288
  }
250
289
  }
290
+ async recover(input) {
291
+ const expected = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(input.canonicalRepository), 'runs', input.runId, 'attempts', input.invocation.attemptId, 'report.json');
292
+ if (resolve(input.invocation.reportPath) !== expected) {
293
+ throw new Error('review feedback implementation report path is not attempt-owned');
294
+ }
295
+ let bytes;
296
+ try {
297
+ bytes = await readRegularFile(expected);
298
+ }
299
+ catch (error) {
300
+ if (isErrorCode(error, 'ENOENT'))
301
+ return undefined;
302
+ throw error;
303
+ }
304
+ return {
305
+ kind: 'completed',
306
+ attemptId: input.invocation.attemptId,
307
+ report: decodeAgentReportForValidation(bytes),
308
+ };
309
+ }
251
310
  }
252
311
  export class ContainedProofAgent {
253
312
  dependencies;
@@ -347,11 +406,43 @@ export class ContainedProofAgent {
347
406
  }
348
407
  }
349
408
  }
409
+ export async function resolveCodexExecutable(command, safePath) {
410
+ if (command.length === 0 || safePath.length === 0)
411
+ throw new Error('Codex command and safe path must be non-empty');
412
+ if (!isAbsolute(command) && command.includes('/'))
413
+ throw new Error('relative Codex command paths are not supported');
414
+ const candidates = isAbsolute(command)
415
+ ? [command]
416
+ : safePath.split(':').filter((entry) => entry.length > 0).map((entry) => join(entry, command));
417
+ for (const candidate of candidates) {
418
+ try {
419
+ const canonical = await realpath(candidate);
420
+ const stat = await lstat(canonical);
421
+ if (!stat.isFile())
422
+ continue;
423
+ await access(canonical, constants.X_OK);
424
+ return canonical;
425
+ }
426
+ catch {
427
+ // Try the next safe-path entry.
428
+ }
429
+ }
430
+ throw new Error('Codex executable is unavailable in the configured safe path');
431
+ }
350
432
  export function createV2Runtime(input) {
351
433
  const targetRoot = resolve(input.targetRoot);
352
434
  const orchestratorHome = resolve(input.orchestratorHome);
353
435
  const now = input.now ?? (() => new Date().toISOString());
354
436
  const commandExecutor = input.processExecutor ?? defaultProcessExecutor;
437
+ const runtimeSafePath = requireRuntimeString(input.safePath, 'safePath');
438
+ let codexExecutable;
439
+ const resolveRuntimeCodex = async (command) => {
440
+ if (codexExecutable?.command === command)
441
+ return codexExecutable.path;
442
+ const path = await resolveCodexExecutable(command, runtimeSafePath);
443
+ codexExecutable = { command, path };
444
+ return path;
445
+ };
355
446
  const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor);
356
447
  const proofFreshnessGit = git;
357
448
  const controller = new AbortController();
@@ -369,7 +460,7 @@ export function createV2Runtime(input) {
369
460
  config: () => requireConfig(currentConfig),
370
461
  orchestratorHome,
371
462
  parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
372
- safePath: requireRuntimeString(input.safePath, 'safePath'),
463
+ safePath: runtimeSafePath,
373
464
  bootId: input.bootId,
374
465
  process: containedProcess,
375
466
  createAttemptId: input.createAttemptId,
@@ -676,7 +767,11 @@ export function createV2Runtime(input) {
676
767
  if (resolve(requestedRoot) !== targetRoot)
677
768
  throw new Error('runtime target root mismatch');
678
769
  const bytes = await readRegularFile(join(targetRoot, '.codex-orchestrator', 'config.json'));
679
- const config = parseAgentAutoConfig(parseJsonWithoutDuplicateKeys(bytes.toString('utf8')));
770
+ const parsed = parseAgentAutoConfig(parseJsonWithoutDuplicateKeys(bytes.toString('utf8')));
771
+ const config = {
772
+ ...parsed,
773
+ codex: { ...parsed.codex, command: await resolveRuntimeCodex(parsed.codex.command) },
774
+ };
680
775
  const path = join(targetRoot, config.runner.stateDir, 'v2', 'run-state.json');
681
776
  if (!runRecords)
682
777
  runRecords = new FileRunRecordWriter(path);
@@ -744,14 +839,17 @@ export function createV2Runtime(input) {
744
839
  readConfig,
745
840
  validateContainment: async (config) => {
746
841
  const certificate = await readContainmentCertificate(containmentCertificatePath(orchestratorHome));
747
- if (certificate.packageVersion !== input.packageVersion)
748
- throw new Error('containment package version mismatch');
749
- if (certificate.argvPolicySha256 !== containmentArgvPolicySha256())
750
- throw new Error('containment argv policy mismatch');
751
- const version = await commandExecutor(config.codex.command, ['--version']);
752
- if (version.exitCode !== 0 || version.stdout.trim() !== `codex-cli ${config.codex.requiredVersion}`) {
753
- throw new Error('current Codex version does not match the containment certificate');
754
- }
842
+ const executablePath = await resolveRuntimeCodex(config.codex.command);
843
+ const version = await commandExecutor(executablePath, ['--version']);
844
+ if (version.exitCode !== 0)
845
+ throw new Error('Codex version is unavailable');
846
+ assertContainmentCertificateMatchesRuntime(certificate, {
847
+ codexVersion: version.stdout.trim(),
848
+ codexExecutablePath: executablePath,
849
+ codexExecutableSha256: sha256(await readFile(executablePath)),
850
+ packageVersion: input.packageVersion,
851
+ argvPolicySha256: containmentArgvPolicySha256(),
852
+ });
755
853
  },
756
854
  ownerLock: {
757
855
  acquire: async ({ canonicalRepository }) => acquireOwnerLock({
@@ -797,10 +895,26 @@ export function createV2Runtime(input) {
797
895
  if (matches.length > 1)
798
896
  throw new Error('multiple open pull requests match publication intent');
799
897
  const match = matches[0];
800
- return match ? { url: match.url, body: match.body } : undefined;
898
+ if (!match)
899
+ return undefined;
900
+ const reviewTarget = await input.pullRequests.getReviewTarget(match.number);
901
+ return {
902
+ url: match.url,
903
+ body: match.body,
904
+ number: match.number,
905
+ nodeId: match.nodeId,
906
+ ...(reviewTarget ? { headSha: reviewTarget.headRefOid } : {}),
907
+ };
801
908
  },
802
909
  createDraft: async ({ title, body, headBranch, baseBranch }) => input.pullRequests.createDraftPullRequest({ title, body, headBranch, baseBranch }),
910
+ listConversationComments: async (number) => (await input.pullRequests.listConversationComments(number))
911
+ .map((comment) => ({ id: comment.id, body: comment.body })),
912
+ postConversationComment: async (number, body) => {
913
+ const comment = await input.pullRequests.postConversationComment(number, body);
914
+ return { id: comment.id, body: comment.body };
915
+ },
803
916
  },
917
+ reviewFeedback: new ReviewFeedbackCoordinator({ pullRequests: input.pullRequests, issues: input.issues, now }),
804
918
  git,
805
919
  routeCoordinator: {
806
920
  run: ({ state, ...routeInput }) => new RouteCoordinator({
@@ -1070,7 +1184,7 @@ async function prepareContainedAttempt(input) {
1070
1184
  bootId: input.bootId,
1071
1185
  });
1072
1186
  const reportOnly = input.operationId === 'triage' || input.operationId === 'ambiguity-review'
1073
- || input.operationId === 'cleanup-review' || input.operationId === 'code-review' || input.operationId === 'spec-review';
1187
+ || input.operationId === 'code-review' || input.operationId === 'spec-review';
1074
1188
  if ((reportOnly
1075
1189
  ? snapshot.policy.sandboxMode !== 'read-only'
1076
1190
  || snapshot.policy.worktreeAccess !== 'read-only'
@@ -1194,56 +1308,135 @@ async function waitForProcessGroupAbsent(processGroupId) {
1194
1308
  }
1195
1309
  }
1196
1310
  export async function materializeReportReadView(input) {
1311
+ await assertDisjointReadViewPaths(input.worktreePath, input.destination);
1312
+ const deniedPaths = normalizeReadViewDeniedPaths(input.deniedPaths);
1197
1313
  await rm(input.destination, { recursive: true, force: true });
1198
1314
  await mkdir(input.destination, { recursive: true, mode: 0o700 });
1199
- await extractHeadArchive(input.worktreePath, input.destination);
1200
- for (const relativePath of input.deniedPaths) {
1201
- if (isAbsolute(relativePath))
1315
+ await materializeCurrentReadView(input.worktreePath, input.destination, deniedPaths);
1316
+ await scrubReportReadView(input.destination);
1317
+ return input.destination;
1318
+ }
1319
+ async function assertDisjointReadViewPaths(worktreePath, destination) {
1320
+ const worktree = await realpath(resolve(worktreePath));
1321
+ const readView = await canonicalProspectivePath(destination);
1322
+ if (isPathWithin(worktree, readView) || isPathWithin(readView, worktree)) {
1323
+ throw new Error('report read-view destination must not overlap its source worktree');
1324
+ }
1325
+ }
1326
+ async function canonicalProspectivePath(path) {
1327
+ let current = resolve(path);
1328
+ const suffix = [];
1329
+ while (true) {
1330
+ try {
1331
+ return resolve(await realpath(current), ...suffix);
1332
+ }
1333
+ catch (error) {
1334
+ if (!isErrorCode(error, 'ENOENT'))
1335
+ throw error;
1336
+ const parent = dirname(current);
1337
+ if (parent === current)
1338
+ throw error;
1339
+ suffix.unshift(basename(current));
1340
+ current = parent;
1341
+ }
1342
+ }
1343
+ }
1344
+ function isPathWithin(parent, child) {
1345
+ const contained = relative(parent, child);
1346
+ return contained === '' || (contained !== '..' && !contained.startsWith('../') && !isAbsolute(contained));
1347
+ }
1348
+ function normalizeReadViewDeniedPaths(paths) {
1349
+ return paths.flatMap((path) => {
1350
+ if (isAbsolute(path))
1351
+ return [];
1352
+ if (path.length === 0 || posix.normalize(path) !== path
1353
+ || path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
1354
+ throw new Error('report read-view denied path escapes destination');
1355
+ }
1356
+ return [path];
1357
+ });
1358
+ }
1359
+ async function materializeCurrentReadView(worktreePath, destination, deniedPaths) {
1360
+ const paths = new Set(await readGitPathList(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']));
1361
+ for (const path of [...paths].sort()) {
1362
+ if (isExcludedReadViewPath(path, deniedPaths))
1202
1363
  continue;
1203
- const deniedPath = resolve(input.destination, relativePath);
1204
- const contained = relative(input.destination, deniedPath);
1364
+ const target = resolve(destination, path);
1365
+ const contained = relative(destination, target);
1205
1366
  if (contained === '' || contained === '..' || contained.startsWith('../') || isAbsolute(contained)) {
1206
- throw new Error('report read-view denied path escapes destination');
1367
+ throw new Error('report read-view path escapes destination');
1368
+ }
1369
+ const source = resolve(worktreePath, path);
1370
+ let metadata;
1371
+ try {
1372
+ metadata = await lstat(source);
1373
+ }
1374
+ catch (error) {
1375
+ if (!isErrorCode(error, 'ENOENT'))
1376
+ throw error;
1377
+ await rm(target, { recursive: true, force: true });
1378
+ continue;
1207
1379
  }
1208
- await rm(deniedPath, { recursive: true, force: true });
1380
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
1381
+ await rm(target, { recursive: true, force: true });
1382
+ continue;
1383
+ }
1384
+ await copyRegularFileNoFollow(source, target, metadata.mode);
1209
1385
  }
1210
- await scrubReportReadView(input.destination);
1211
- return input.destination;
1212
1386
  }
1213
- async function extractHeadArchive(worktreePath, destination) {
1214
- await new Promise((resolveExtract, reject) => {
1215
- const archive = spawn('git', ['-C', worktreePath, 'archive', '--format=tar', 'HEAD'], { stdio: ['ignore', 'pipe', 'pipe'] });
1216
- const extract = spawn('tar', ['-xf', '-', '-C', destination], { stdio: ['pipe', 'ignore', 'pipe'] });
1217
- const failures = [];
1218
- archive.once('error', reject);
1219
- extract.once('error', reject);
1220
- archive.stderr.on('data', (chunk) => {
1221
- if (failures.join('').length < 8 * 1024)
1222
- failures.push(chunk.toString('utf8'));
1223
- });
1224
- extract.stderr.on('data', (chunk) => {
1225
- if (failures.join('').length < 8 * 1024)
1226
- failures.push(chunk.toString('utf8'));
1227
- });
1228
- archive.stdout.pipe(extract.stdin);
1229
- let archiveCode = null;
1230
- let extractCode = null;
1231
- const settle = () => {
1232
- if (archiveCode === null || extractCode === null)
1387
+ function isExcludedReadViewPath(path, deniedPaths) {
1388
+ if (path.split('/').some((segment) => segment.startsWith('.env')))
1389
+ return true;
1390
+ return deniedPaths.some((deniedPath) => path === deniedPath || path.startsWith(`${deniedPath}/`));
1391
+ }
1392
+ async function readGitPathList(worktreePath, args) {
1393
+ return new Promise((resolvePaths, reject) => {
1394
+ const child = spawn('git', ['-C', worktreePath, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
1395
+ const stdout = [];
1396
+ const stderr = [];
1397
+ child.once('error', reject);
1398
+ child.stdout.on('data', (chunk) => { stdout.push(chunk); });
1399
+ child.stderr.on('data', (chunk) => { stderr.push(chunk); });
1400
+ child.once('close', (code) => {
1401
+ if (code !== 0) {
1402
+ reject(new Error(`report read-view inventory failed: ${Buffer.concat(stderr).toString('utf8').slice(0, 8 * 1024)}`));
1233
1403
  return;
1234
- if (archiveCode !== 0 || extractCode !== 0)
1235
- reject(new Error(`report read-view archive failed: ${failures.join('').slice(0, 8 * 1024)}`));
1236
- else
1237
- resolveExtract();
1238
- };
1239
- archive.once('close', (code) => { archiveCode = code ?? 1; settle(); });
1240
- extract.once('close', (code) => { extractCode = code ?? 1; settle(); });
1404
+ }
1405
+ resolvePaths(Buffer.concat(stdout).toString('utf8').split('\0').filter((path) => path.length > 0));
1406
+ });
1241
1407
  });
1242
1408
  }
1409
+ async function copyRegularFileNoFollow(sourcePath, targetPath, mode) {
1410
+ const source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
1411
+ let target;
1412
+ try {
1413
+ const metadata = await source.stat();
1414
+ if (!metadata.isFile())
1415
+ throw new Error('report read-view source must be a regular file');
1416
+ await rm(targetPath, { recursive: true, force: true });
1417
+ await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
1418
+ target = await open(targetPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode & 0o777);
1419
+ const buffer = Buffer.allocUnsafe(64 * 1024);
1420
+ while (true) {
1421
+ const { bytesRead } = await source.read(buffer, 0, buffer.length, null);
1422
+ if (bytesRead === 0)
1423
+ break;
1424
+ let offset = 0;
1425
+ while (offset < bytesRead) {
1426
+ const { bytesWritten } = await target.write(buffer, offset, bytesRead - offset, null);
1427
+ offset += bytesWritten;
1428
+ }
1429
+ }
1430
+ }
1431
+ finally {
1432
+ await target?.close();
1433
+ await source.close();
1434
+ }
1435
+ }
1243
1436
  async function scrubReportReadView(directory) {
1244
1437
  for (const entry of await readdir(directory, { withFileTypes: true })) {
1245
1438
  const path = join(directory, entry.name);
1246
- if (entry.name === '.env' || entry.name.startsWith('.env.') || entry.isSymbolicLink()) {
1439
+ if (entry.name.startsWith('.env') || entry.isSymbolicLink()) {
1247
1440
  await rm(path, { recursive: true, force: true });
1248
1441
  }
1249
1442
  else if (entry.isDirectory()) {