codex-orchestrator 2.0.10 → 2.0.11

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 (109) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +26 -1
  3. package/dist/src/index.d.ts +3 -2
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +1 -1
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/v2/acceptance-proof.d.ts +29 -16
  8. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  9. package/dist/src/v2/acceptance-proof.js +159 -129
  10. package/dist/src/v2/acceptance-proof.js.map +1 -1
  11. package/dist/src/v2/adapters/command.d.ts +1 -0
  12. package/dist/src/v2/adapters/command.d.ts.map +1 -1
  13. package/dist/src/v2/adapters/command.js +1 -1
  14. package/dist/src/v2/adapters/command.js.map +1 -1
  15. package/dist/src/v2/adapters/gh-pull-request-adapter.js +1 -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 +1 -0
  18. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  19. package/dist/src/v2/adapters/pull-requests.js +1 -0
  20. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  21. package/dist/src/v2/adapters/worktree.js +3 -3
  22. package/dist/src/v2/adapters/worktree.js.map +1 -1
  23. package/dist/src/v2/atomic-store.d.ts +15 -0
  24. package/dist/src/v2/atomic-store.d.ts.map +1 -1
  25. package/dist/src/v2/atomic-store.js +55 -8
  26. package/dist/src/v2/atomic-store.js.map +1 -1
  27. package/dist/src/v2/candidate.d.ts +152 -0
  28. package/dist/src/v2/candidate.d.ts.map +1 -0
  29. package/dist/src/v2/candidate.js +129 -0
  30. package/dist/src/v2/candidate.js.map +1 -0
  31. package/dist/src/v2/checked-change.d.ts +37 -6
  32. package/dist/src/v2/checked-change.d.ts.map +1 -1
  33. package/dist/src/v2/checked-change.js +71 -0
  34. package/dist/src/v2/checked-change.js.map +1 -1
  35. package/dist/src/v2/cli.d.ts +6 -0
  36. package/dist/src/v2/cli.d.ts.map +1 -1
  37. package/dist/src/v2/cli.js +57 -21
  38. package/dist/src/v2/cli.js.map +1 -1
  39. package/dist/src/v2/codex-process.d.ts +1 -0
  40. package/dist/src/v2/codex-process.d.ts.map +1 -1
  41. package/dist/src/v2/codex-process.js +12 -1
  42. package/dist/src/v2/codex-process.js.map +1 -1
  43. package/dist/src/v2/contained-report-operation.d.ts +196 -30
  44. package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
  45. package/dist/src/v2/contained-report-operation.js +464 -150
  46. package/dist/src/v2/contained-report-operation.js.map +1 -1
  47. package/dist/src/v2/direct-delivery.d.ts +10 -28
  48. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  49. package/dist/src/v2/direct-delivery.js +44 -114
  50. package/dist/src/v2/direct-delivery.js.map +1 -1
  51. package/dist/src/v2/implementation-reviewer.d.ts +4 -24
  52. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
  53. package/dist/src/v2/implementation-reviewer.js +29 -35
  54. package/dist/src/v2/implementation-reviewer.js.map +1 -1
  55. package/dist/src/v2/owner-control-lock.d.ts +13 -1
  56. package/dist/src/v2/owner-control-lock.d.ts.map +1 -1
  57. package/dist/src/v2/owner-control-lock.js +66 -7
  58. package/dist/src/v2/owner-control-lock.js.map +1 -1
  59. package/dist/src/v2/proof-report.d.ts +2 -1
  60. package/dist/src/v2/proof-report.d.ts.map +1 -1
  61. package/dist/src/v2/proof-report.js +6 -40
  62. package/dist/src/v2/proof-report.js.map +1 -1
  63. package/dist/src/v2/proof-store.d.ts +19 -7
  64. package/dist/src/v2/proof-store.d.ts.map +1 -1
  65. package/dist/src/v2/proof-store.js +162 -41
  66. package/dist/src/v2/proof-store.js.map +1 -1
  67. package/dist/src/v2/review-feedback-coordinator.d.ts +7 -1
  68. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
  69. package/dist/src/v2/review-feedback-coordinator.js +97 -70
  70. package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
  71. package/dist/src/v2/review-feedback.d.ts +0 -18
  72. package/dist/src/v2/review-feedback.d.ts.map +1 -1
  73. package/dist/src/v2/review-feedback.js +9 -34
  74. package/dist/src/v2/review-feedback.js.map +1 -1
  75. package/dist/src/v2/route-coordinator.d.ts +7 -12
  76. package/dist/src/v2/route-coordinator.d.ts.map +1 -1
  77. package/dist/src/v2/route-coordinator.js +33 -128
  78. package/dist/src/v2/route-coordinator.js.map +1 -1
  79. package/dist/src/v2/route-decision.d.ts +0 -9
  80. package/dist/src/v2/route-decision.d.ts.map +1 -1
  81. package/dist/src/v2/route-decision.js +7 -27
  82. package/dist/src/v2/route-decision.js.map +1 -1
  83. package/dist/src/v2/run-issue.d.ts +64 -26
  84. package/dist/src/v2/run-issue.d.ts.map +1 -1
  85. package/dist/src/v2/run-issue.js +1298 -620
  86. package/dist/src/v2/run-issue.js.map +1 -1
  87. package/dist/src/v2/run-store.d.ts +39 -25
  88. package/dist/src/v2/run-store.d.ts.map +1 -1
  89. package/dist/src/v2/run-store.js +469 -88
  90. package/dist/src/v2/run-store.js.map +1 -1
  91. package/dist/src/v2/runtime.d.ts +61 -36
  92. package/dist/src/v2/runtime.d.ts.map +1 -1
  93. package/dist/src/v2/runtime.js +992 -402
  94. package/dist/src/v2/runtime.js.map +1 -1
  95. package/dist/src/v2/spec-coordinator.d.ts +16 -28
  96. package/dist/src/v2/spec-coordinator.d.ts.map +1 -1
  97. package/dist/src/v2/spec-coordinator.js +81 -43
  98. package/dist/src/v2/spec-coordinator.js.map +1 -1
  99. package/dist/src/v2/spec-delivery.d.ts +3 -34
  100. package/dist/src/v2/spec-delivery.d.ts.map +1 -1
  101. package/dist/src/v2/spec-delivery.js +42 -107
  102. package/dist/src/v2/spec-delivery.js.map +1 -1
  103. package/docs/deep-dive.md +52 -14
  104. package/internal-workflow/evals/coding-skill-evals.json +58 -17
  105. package/internal-workflow/manifest.json +1 -1
  106. package/internal-workflow/schemas/proof-report-v1.json +1 -1
  107. package/internal-workflow/skills/code-review/SKILL.md +18 -0
  108. package/internal-workflow/skills/spec-implementer/SKILL.md +8 -1
  109. package/package.json +1 -1
@@ -1,9 +1,4 @@
1
- import { canonicalJson, containsCredentialEvidence, sha256 } from './containment.js';
2
- import { decodeAgentReportForValidation } from './report-envelope.js';
3
- import { hashCodeReviewReport, validateCodeReviewReport, } from './code-review-report.js';
4
- import { hashAmbiguityReviewArtifact, hashTriageArtifact, validateAmbiguityReviewArtifact, } from './route-decision.js';
5
- import { validateTriageRoute } from './triage-route.js';
6
- const MAX_STRING_LENGTH = 16 * 1024;
1
+ import { canonicalJson, sha256 } from './containment.js';
7
2
  const POLICY_KEYS = [
8
3
  'sandboxMode', 'cwdClass', 'worktreeAccess', 'writableRootClasses', 'runnerPostcondition',
9
4
  'network', 'networkHosts', 'mcpTools', 'approvalCeiling', 'externalWrite',
@@ -14,171 +9,494 @@ export class InjectedContainedReportOperation {
14
9
  this.dependencies = dependencies;
15
10
  }
16
11
  async run(input) {
17
- let before;
12
+ const existing = await input.invocationState.read();
13
+ if (existing)
14
+ return this.recover(input, validateDurableReportInvocation(existing));
15
+ return this.launch(input);
16
+ }
17
+ async launch(input) {
18
+ let baseline;
18
19
  try {
19
- before = requireReportSnapshot(await this.dependencies.snapshot(input.worktreePath));
20
+ baseline = requireReportSnapshot(await this.dependencies.snapshot(input.worktreePath, input.operation));
20
21
  }
21
22
  catch {
22
- return { status: 'blocked', kind: 'safety', code: 'report-operation-snapshot-failed' };
23
+ return blocked('safety', 'report-operation-snapshot-failed');
23
24
  }
25
+ const attemptId = this.dependencies.createAttemptId();
26
+ if (!attemptId || input.forbiddenAttemptIds?.includes(attemptId))
27
+ return blocked('safety', 'report-operation-attempt-identity-invalid');
24
28
  let attempt;
25
29
  try {
26
- attempt = await this.dependencies.prepare({
27
- operation: input.operation,
28
- attemptId: input.attemptId,
29
- runId: input.runId,
30
- workflowGeneration: input.workflowGeneration,
30
+ attempt = await this.prepare(input, attemptId);
31
+ }
32
+ catch (error) {
33
+ return error instanceof ReportAuthorityError
34
+ ? blocked('safety', 'report-operation-authority-drift')
35
+ : blocked('external', 'report-operation-prepare-failed');
36
+ }
37
+ const prepared = {
38
+ version: 1, operation: input.operation, attemptId, generationHash: input.workflowGeneration.generationHash,
39
+ promptFactsSha256: promptFactsSha256(input.promptFacts),
40
+ reportPath: attempt.reportPath, phase: 'prepared', host: this.dependencies.host, bootId: this.dependencies.bootId,
41
+ preparedAt: this.dependencies.now(), launchedAt: null, pid: null, processStartIdentity: null,
42
+ processGroupId: null, baseline,
43
+ };
44
+ if (!await input.invocationState.compareAndSwap(undefined, prepared))
45
+ return blocked('safety', 'report-operation-state-conflict');
46
+ let current = prepared;
47
+ let result;
48
+ try {
49
+ await input.beforeLaunch?.();
50
+ }
51
+ catch (error) {
52
+ const abandoned = await this.abandon(input, current, attempt);
53
+ if (abandoned)
54
+ return abandoned;
55
+ throw error;
56
+ }
57
+ try {
58
+ result = await this.dependencies.launch({
59
+ ...input,
60
+ attempt,
61
+ onSpawned: async ({ pid, processGroupId }) => {
62
+ const processStartIdentity = await this.dependencies.processStartIdentity(pid);
63
+ if (!processStartIdentity)
64
+ throw new Error('report process identity unavailable');
65
+ const launched = {
66
+ ...prepared, phase: 'launched', launchedAt: this.dependencies.now(), pid, processStartIdentity, processGroupId,
67
+ };
68
+ if (!await input.invocationState.compareAndSwap(prepared, launched))
69
+ throw new Error('report launch state conflict');
70
+ current = launched;
71
+ },
31
72
  });
32
73
  }
33
74
  catch {
34
- return this.finishWithSnapshot(input.worktreePath, before, {
35
- status: 'blocked', kind: 'external', code: 'report-operation-prepare-failed',
36
- });
75
+ result = { status: 'retryable', code: 'report-operation-launch-failed' };
37
76
  }
38
- let launchResult;
39
- if (!hasExactReadOnlyAuthority(attempt, input)) {
40
- launchResult = { status: 'blocked', kind: 'safety', code: 'report-operation-authority-drift' };
41
- }
42
- else {
43
- if (isImplementationReview(input.operation)) {
44
- if (!input.reviewContext || !input.onPrepared || !input.onLaunched) {
45
- return this.finishWithSnapshot(input.worktreePath, before, {
46
- status: 'blocked', kind: 'safety', code: 'review-operation-launch-gate-missing',
47
- });
48
- }
49
- try {
50
- await input.onPrepared();
51
- }
52
- catch {
53
- return this.finishWithSnapshot(input.worktreePath, before, {
54
- status: 'blocked', kind: 'safety', code: 'review-operation-prepare-persistence-failed',
55
- });
56
- }
57
- }
58
- try {
59
- launchResult = await this.dependencies.launch({ ...input, attempt });
60
- }
61
- catch {
62
- launchResult = { status: 'blocked', kind: 'external', code: 'report-operation-launch-failed' };
77
+ if (result.status === 'completed')
78
+ return this.finish(input, current, attempt, result.reportBytes);
79
+ if (result.status === 'safe-halt')
80
+ return { status: 'safe-halt', code: 'report-operation-process-unresolved' };
81
+ if (result.status === 'cancelled' || result.status === 'blocked') {
82
+ const abandoned = await this.abandon(input, current, attempt);
83
+ if (abandoned)
84
+ return abandoned;
85
+ return result;
86
+ }
87
+ const recovered = await this.observeReport(current.reportPath);
88
+ if (recovered.status === 'unknown')
89
+ return { status: 'safe-halt', code: 'report-operation-report-observation-unknown' };
90
+ if (recovered.status === 'available')
91
+ return this.finish(input, current, attempt, recovered.bytes);
92
+ const abandoned = await this.abandon(input, current, attempt);
93
+ if (abandoned)
94
+ return abandoned;
95
+ return result;
96
+ }
97
+ async recover(input, invocation) {
98
+ if (invocation.operation !== input.operation || invocation.generationHash !== input.workflowGeneration.generationHash
99
+ || invocation.host !== this.dependencies.host || invocation.bootId !== this.dependencies.bootId) {
100
+ return { status: 'safe-halt', code: 'report-operation-owner-identity-unresolved' };
101
+ }
102
+ if (invocation.promptFactsSha256 !== promptFactsSha256(input.promptFacts)) {
103
+ return { status: 'safe-halt', code: 'report-operation-prompt-facts-drift' };
104
+ }
105
+ let attempt;
106
+ try {
107
+ attempt = await this.prepare(input, invocation.attemptId);
108
+ }
109
+ catch (error) {
110
+ return error instanceof ReportAuthorityError
111
+ ? blocked('safety', 'report-operation-authority-drift')
112
+ : blocked('external', 'report-operation-prepare-failed');
113
+ }
114
+ if (attempt.reportPath !== invocation.reportPath)
115
+ return blocked('safety', 'report-operation-report-path-drift');
116
+ const report = await this.observeReport(invocation.reportPath);
117
+ if (report.status === 'unknown')
118
+ return { status: 'safe-halt', code: 'report-operation-report-observation-unknown' };
119
+ if (invocation.phase === 'prepared') {
120
+ if (report.status === 'available')
121
+ return this.finish(input, invocation, attempt, report.bytes);
122
+ if (!await this.settleAttempt(attempt))
123
+ return { status: 'safe-halt', code: 'report-operation-attempt-cleanup-failed' };
124
+ if (!await input.invocationState.compareAndSwap(invocation, undefined))
125
+ return blocked('safety', 'report-operation-state-conflict');
126
+ return { status: 'retryable', code: 'report-operation-prepared-attempt-abandoned' };
127
+ }
128
+ const unresolved = await this.unresolvedProcess(invocation);
129
+ if (unresolved)
130
+ return unresolved;
131
+ if (report.status === 'available')
132
+ return this.finish(input, invocation, attempt, report.bytes);
133
+ if (!await this.settleAttempt(attempt))
134
+ return { status: 'safe-halt', code: 'report-operation-attempt-cleanup-failed' };
135
+ if (!await input.invocationState.compareAndSwap(invocation, undefined))
136
+ return blocked('safety', 'report-operation-state-conflict');
137
+ return { status: 'retryable', code: 'report-operation-output-unavailable' };
138
+ }
139
+ async abandon(input, invocation, attempt) {
140
+ if (invocation.phase === 'launched') {
141
+ const unresolved = await this.unresolvedProcess(invocation);
142
+ if (unresolved)
143
+ return unresolved;
144
+ }
145
+ if (!await this.settleAttempt(attempt))
146
+ return { status: 'safe-halt', code: 'report-operation-attempt-cleanup-failed' };
147
+ if (!await input.invocationState.compareAndSwap(invocation, undefined))
148
+ return blocked('safety', 'report-operation-state-conflict');
149
+ return undefined;
150
+ }
151
+ async observeReport(path) {
152
+ try {
153
+ return await this.dependencies.readReport(path);
154
+ }
155
+ catch {
156
+ return { status: 'unknown' };
157
+ }
158
+ }
159
+ async unresolvedProcess(invocation) {
160
+ const observed = await this.dependencies.inspectProcess(invocation).catch(() => ({ status: 'unknown' }));
161
+ if (observed.status === 'unknown')
162
+ return { status: 'safe-halt', code: 'report-operation-process-observation-unknown' };
163
+ if (observed.status === 'present' && (observed.processStartIdentity === invocation.processStartIdentity || observed.processGroupAlive !== false)) {
164
+ return { status: 'safe-halt', code: 'report-operation-process-active-or-uncertain' };
165
+ }
166
+ return observed.status === 'absent' && observed.processGroupAlive !== false
167
+ ? { status: 'safe-halt', code: 'report-operation-process-group-unresolved' } : undefined;
168
+ }
169
+ async settleAttempt(attempt) {
170
+ try {
171
+ await this.dependencies.settleAttempt(attempt);
172
+ return true;
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
178
+ async prepare(input, attemptId) {
179
+ const attempt = await this.dependencies.prepare({
180
+ operation: input.operation, attemptId, runId: input.runId, workflowGeneration: input.workflowGeneration,
181
+ });
182
+ if (!hasExactReportAuthority(attempt, input) || !attempt.reportPath)
183
+ throw new ReportAuthorityError();
184
+ return attempt;
185
+ }
186
+ async finish(input, invocation, attempt, reportBytes) {
187
+ let after;
188
+ try {
189
+ after = requireReportSnapshot(await this.dependencies.snapshot(input.worktreePath, input.operation));
190
+ }
191
+ catch {
192
+ return blocked('safety', 'report-operation-snapshot-failed');
193
+ }
194
+ if (canonicalJson(invocation.baseline) !== canonicalJson(after))
195
+ return blocked('safety', 'report-operation-worktree-mutated');
196
+ if (!await this.settleAttempt(attempt))
197
+ return { status: 'safe-halt', code: 'report-operation-attempt-cleanup-failed' };
198
+ return { status: 'completed', attemptId: invocation.attemptId, reportBytes: Buffer.from(reportBytes), reportSha256: sha256(reportBytes) };
199
+ }
200
+ }
201
+ export class InjectedContainedMutableOperation {
202
+ dependencies;
203
+ constructor(dependencies) {
204
+ this.dependencies = dependencies;
205
+ }
206
+ async run(input) {
207
+ const existing = await input.invocationState.read();
208
+ return existing ? this.recover(input, validateDurableMutableInvocation(existing)) : this.launch(input);
209
+ }
210
+ async settle(input) {
211
+ const existing = await input.invocationState.read();
212
+ if (!existing)
213
+ return { status: 'settled' };
214
+ const recovered = await this.recover(input, validateDurableMutableInvocation(existing));
215
+ if (recovered.status === 'retryable') {
216
+ return await input.invocationState.read() === undefined
217
+ ? { status: 'settled' }
218
+ : { status: 'safe-halt', code: 'mutable-operation-state-conflict' };
219
+ }
220
+ if (recovered.status === 'completed') {
221
+ const adopted = await input.invocationState.read();
222
+ if (!adopted || adopted.attemptId !== recovered.attemptId || adopted.phase !== 'adopted') {
223
+ return { status: 'safe-halt', code: 'mutable-operation-state-conflict' };
63
224
  }
225
+ return await input.invocationState.compareAndSwap(adopted, undefined)
226
+ ? { status: 'settled' }
227
+ : { status: 'safe-halt', code: 'mutable-operation-state-conflict' };
64
228
  }
65
- if (launchResult.status === 'safe-halt') {
66
- return {
67
- status: 'safe-halt',
68
- process: {
69
- pid: launchResult.pid,
70
- processGroupId: launchResult.processGroupId,
71
- startedAt: launchResult.startedAt,
72
- baseline: structuredClone(before),
229
+ return { status: 'safe-halt', code: recovered.status === 'cancelled'
230
+ ? 'mutable-operation-settlement-cancelled' : recovered.code };
231
+ }
232
+ async launch(input) {
233
+ let baseline;
234
+ try {
235
+ baseline = requireReportSnapshot(await this.dependencies.snapshot(input.worktreePath));
236
+ }
237
+ catch {
238
+ return blocked('safety', 'mutable-operation-snapshot-failed');
239
+ }
240
+ const attemptId = this.dependencies.createAttemptId();
241
+ if (!attemptId)
242
+ return blocked('safety', 'mutable-operation-attempt-identity-invalid');
243
+ let attempt;
244
+ try {
245
+ attempt = await this.prepare(input, attemptId);
246
+ }
247
+ catch (error) {
248
+ return error instanceof ReportAuthorityError
249
+ ? blocked('safety', 'mutable-operation-authority-drift') : blocked('external', 'mutable-operation-prepare-failed');
250
+ }
251
+ const prepared = {
252
+ version: 1, operation: input.operation, attemptId, generationHash: input.workflowGeneration.generationHash,
253
+ promptFactsSha256: promptFactsSha256(input.promptFacts), worktreePath: input.worktreePath, reportPath: attempt.reportPath,
254
+ context: structuredClone(input.context),
255
+ phase: 'prepared', host: this.dependencies.host, bootId: this.dependencies.bootId, preparedAt: this.dependencies.now(),
256
+ launchedAt: null, pid: null, processStartIdentity: null, processGroupId: null, baseline, reportSha256: null, resultSnapshot: null,
257
+ };
258
+ if (!await input.invocationState.compareAndSwap(undefined, prepared))
259
+ return blocked('safety', 'mutable-operation-state-conflict');
260
+ let current = prepared;
261
+ let result;
262
+ try {
263
+ await input.beforeLaunch?.();
264
+ result = await this.dependencies.launch({
265
+ ...input, attempt,
266
+ onSpawned: async ({ pid, processGroupId }) => {
267
+ const processStartIdentity = await this.dependencies.processStartIdentity(pid);
268
+ if (!processStartIdentity)
269
+ throw new Error('mutable process identity unavailable');
270
+ const launched = { ...prepared, phase: 'launched', launchedAt: this.dependencies.now(),
271
+ pid, processStartIdentity, processGroupId };
272
+ if (!await input.invocationState.compareAndSwap(prepared, launched))
273
+ throw new Error('mutable launch state conflict');
274
+ current = launched;
73
275
  },
74
- waitForAbsence: launchResult.waitForAbsence,
75
- };
276
+ });
277
+ }
278
+ catch {
279
+ return { status: 'retryable', code: 'mutable-operation-launch-failed' };
76
280
  }
77
- return this.finishWithSnapshot(input.worktreePath, before, launchResult, input);
281
+ if (result.status === 'completed')
282
+ return current.phase === 'launched'
283
+ ? this.adopt(input, current, result.reportBytes)
284
+ : { status: 'safe-halt', code: 'mutable-operation-launch-unfenced' };
285
+ if (result.status === 'safe-halt')
286
+ return { status: 'safe-halt', code: 'mutable-operation-process-unresolved' };
287
+ if (result.status === 'cancelled' || result.status === 'blocked') {
288
+ const unresolved = current.phase === 'launched' ? await this.unresolvedProcess(current) : undefined;
289
+ if (unresolved)
290
+ return unresolved;
291
+ const snapshot = await this.stableSnapshot(input.worktreePath);
292
+ if (!snapshot || canonicalJson(snapshot) !== canonicalJson(current.baseline))
293
+ return { status: 'safe-halt', code: 'mutable-operation-abandonment-ambiguous' };
294
+ if (!await input.invocationState.compareAndSwap(current, undefined))
295
+ return blocked('safety', 'mutable-operation-state-conflict');
296
+ }
297
+ return result;
78
298
  }
79
- async finishWithSnapshot(worktreePath, before, launchResult, input) {
80
- let after;
299
+ async recover(input, invocation) {
300
+ if (invocation.operation !== input.operation
301
+ || invocation.generationHash !== input.workflowGeneration.generationHash || invocation.promptFactsSha256 !== promptFactsSha256(input.promptFacts)
302
+ || canonicalJson(invocation.context) !== canonicalJson(input.context)
303
+ || invocation.worktreePath !== input.worktreePath || invocation.host !== this.dependencies.host || invocation.bootId !== this.dependencies.bootId)
304
+ return { status: 'safe-halt', code: 'mutable-operation-correlation-drift' };
305
+ let attempt;
81
306
  try {
82
- after = requireReportSnapshot(await this.dependencies.snapshot(worktreePath));
307
+ attempt = await this.prepare(input, invocation.attemptId);
83
308
  }
84
309
  catch {
85
- return { status: 'blocked', kind: 'safety', code: 'report-operation-snapshot-failed' };
310
+ return { status: 'safe-halt', code: 'mutable-operation-prepare-recovery-failed' };
311
+ }
312
+ if (attempt.reportPath !== invocation.reportPath)
313
+ return { status: 'safe-halt', code: 'mutable-operation-report-path-drift' };
314
+ const report = await this.observeReport(invocation.reportPath);
315
+ if (report.status === 'unknown')
316
+ return { status: 'safe-halt', code: 'mutable-operation-report-observation-unknown' };
317
+ if (invocation.phase === 'adopted') {
318
+ if (report.status !== 'available' || sha256(report.bytes) !== invocation.reportSha256)
319
+ return { status: 'safe-halt', code: 'mutable-operation-adopted-report-drift' };
320
+ const snapshot = await this.stableSnapshot(input.worktreePath);
321
+ if (!snapshot || canonicalJson(snapshot) !== canonicalJson(invocation.resultSnapshot))
322
+ return { status: 'safe-halt', code: 'mutable-operation-adopted-worktree-drift' };
323
+ return this.completeAdopted(input, invocation, report.bytes);
86
324
  }
87
- if (!sameSnapshot(before, after)) {
88
- return { status: 'blocked', kind: 'safety', code: 'report-operation-worktree-mutated' };
325
+ if (invocation.phase === 'prepared') {
326
+ if (report.status === 'available')
327
+ return { status: 'safe-halt', code: 'mutable-operation-prepared-report-ambiguous' };
328
+ const snapshot = await this.stableSnapshot(input.worktreePath);
329
+ if (!snapshot || canonicalJson(snapshot) !== canonicalJson(invocation.baseline))
330
+ return { status: 'safe-halt', code: 'mutable-operation-prepared-worktree-ambiguous' };
331
+ if (!await input.invocationState.compareAndSwap(invocation, undefined))
332
+ return blocked('safety', 'mutable-operation-state-conflict');
333
+ return { status: 'retryable', code: 'mutable-operation-prepared-attempt-abandoned' };
334
+ }
335
+ const unresolved = await this.unresolvedProcess(invocation);
336
+ if (unresolved)
337
+ return unresolved;
338
+ if (report.status === 'available')
339
+ return this.adopt(input, invocation, report.bytes);
340
+ const snapshot = await this.stableSnapshot(input.worktreePath);
341
+ if (!snapshot || canonicalJson(snapshot) !== canonicalJson(invocation.baseline))
342
+ return { status: 'safe-halt', code: 'mutable-operation-worktree-without-report' };
343
+ if (!await input.invocationState.compareAndSwap(invocation, undefined))
344
+ return blocked('safety', 'mutable-operation-state-conflict');
345
+ return { status: 'retryable', code: 'mutable-operation-output-unavailable' };
346
+ }
347
+ async adopt(input, invocation, reportBytes) {
348
+ const resultSnapshot = await this.stableSnapshot(input.worktreePath);
349
+ if (!resultSnapshot || resultSnapshot.worktreeIdentity !== invocation.baseline.worktreeIdentity || resultSnapshot.headSha !== invocation.baseline.headSha)
350
+ return { status: 'safe-halt', code: 'mutable-operation-result-worktree-mismatch' };
351
+ const adopted = { ...invocation, phase: 'adopted', reportSha256: sha256(reportBytes), resultSnapshot };
352
+ if (!await input.invocationState.compareAndSwap(invocation, adopted))
353
+ return blocked('safety', 'mutable-operation-state-conflict');
354
+ return this.completeAdopted(input, adopted, reportBytes);
355
+ }
356
+ async completeAdopted(input, invocation, reportBytes) {
357
+ if (invocation.context.repairOnly && canonicalJson(invocation.baseline) !== canonicalJson(invocation.resultSnapshot)) {
358
+ if (!await input.invocationState.compareAndSwap(invocation, undefined))
359
+ return blocked('safety', 'mutable-operation-state-conflict');
360
+ return blocked('safety', 'report-repair-modified-worktree');
361
+ }
362
+ return completedMutable(invocation, reportBytes);
363
+ }
364
+ async prepare(input, attemptId) {
365
+ const operation = workerOperation(input.operation);
366
+ const attempt = await this.dependencies.prepare({ operation, attemptId, runId: input.runId, workflowGeneration: input.workflowGeneration });
367
+ if (attempt.operation !== operation || attempt.generationHash !== input.workflowGeneration.generationHash || !hasExactMutableAuthority(attempt.policy)
368
+ || !attempt.reportPath)
369
+ throw new ReportAuthorityError();
370
+ return attempt;
371
+ }
372
+ async observeReport(path) {
373
+ try {
374
+ return await this.dependencies.readReport(path);
375
+ }
376
+ catch {
377
+ return { status: 'unknown' };
89
378
  }
90
- if (launchResult.status !== 'completed')
91
- return launchResult;
92
- if (!input)
93
- return { status: 'blocked', kind: 'external', code: 'report-operation-prepare-failed' };
94
- return validateCompletedReport(input.operation, input.attemptId, launchResult.reportBytes, input.reviewContext);
379
+ }
380
+ async stableSnapshot(worktreePath) {
381
+ try {
382
+ const first = requireReportSnapshot(await this.dependencies.snapshot(worktreePath));
383
+ const second = requireReportSnapshot(await this.dependencies.snapshot(worktreePath));
384
+ return canonicalJson(first) === canonicalJson(second) ? first : undefined;
385
+ }
386
+ catch {
387
+ return undefined;
388
+ }
389
+ }
390
+ async unresolvedProcess(invocation) {
391
+ const observed = await this.dependencies.inspectProcess(invocation).catch(() => ({ status: 'unknown' }));
392
+ if (observed.status === 'unknown')
393
+ return { status: 'safe-halt', code: 'mutable-operation-process-observation-unknown' };
394
+ if (observed.status === 'present' && (observed.processStartIdentity === invocation.processStartIdentity || observed.processGroupAlive !== false))
395
+ return { status: 'safe-halt', code: 'mutable-operation-process-active-or-uncertain' };
396
+ return observed.status === 'absent' && observed.processGroupAlive !== false
397
+ ? { status: 'safe-halt', code: 'mutable-operation-process-group-unresolved' } : undefined;
95
398
  }
96
399
  }
97
- function hasExactReadOnlyAuthority(attempt, input) {
98
- const policy = attempt.policy;
99
- return attempt.operation === input.operation
100
- && attempt.generationHash === input.workflowGeneration.generationHash
101
- && hasExactKeys(policy, POLICY_KEYS)
102
- && policy.sandboxMode === 'read-only'
103
- && policy.cwdClass === 'worktree'
104
- && policy.worktreeAccess === 'read-only'
105
- && Array.isArray(policy.writableRootClasses)
106
- && policy.writableRootClasses.length === 0
107
- && policy.runnerPostcondition === 'report-only'
108
- && policy.network === 'deny'
109
- && Array.isArray(policy.networkHosts)
110
- && policy.networkHosts.length === 0
111
- && Array.isArray(policy.mcpTools)
112
- && policy.mcpTools.length === 0
113
- && policy.approvalCeiling === 'never'
114
- && policy.externalWrite === false;
400
+ export function validateDurableMutableInvocation(value) {
401
+ const keys = [
402
+ 'version', 'operation', 'attemptId', 'generationHash', 'promptFactsSha256', 'worktreePath', 'reportPath',
403
+ 'context', 'phase', 'host', 'bootId', 'preparedAt', 'launchedAt', 'pid', 'processStartIdentity', 'processGroupId', 'baseline',
404
+ 'reportSha256', 'resultSnapshot',
405
+ ];
406
+ if (!hasExactKeys(value, keys))
407
+ throw new Error('durable mutable invocation is invalid');
408
+ const record = value;
409
+ if (record.version !== 1 || !['qualification-repair', 'implementation', 'review-feedback-implementation'].includes(record.operation)
410
+ || !nonEmpty(record.attemptId) || !/^[0-9a-f]{64}$/u.test(record.generationHash)
411
+ || !/^[0-9a-f]{64}$/u.test(record.promptFactsSha256) || !nonEmpty(record.worktreePath) || !nonEmpty(record.reportPath)
412
+ || !nonEmpty(record.host) || !nonEmpty(record.bootId) || !timestamp(record.preparedAt)
413
+ || !['prepared', 'launched', 'adopted'].includes(record.phase))
414
+ throw new Error('durable mutable invocation fields are invalid');
415
+ const context = record.context;
416
+ if (!hasExactKeys(context, ['repairOnly', 'reworkFindings']) || typeof context.repairOnly !== 'boolean'
417
+ || !Array.isArray(context.reworkFindings) || !context.reworkFindings.every(nonEmpty)) {
418
+ throw new Error('durable mutable invocation context is invalid');
419
+ }
420
+ const launched = record.phase === 'launched' || record.phase === 'adopted';
421
+ if (launched !== (positive(record.pid) && positive(record.processGroupId) && nonEmpty(record.processStartIdentity) && timestamp(record.launchedAt)))
422
+ throw new Error('durable mutable invocation process identity is invalid');
423
+ if (!launched && (record.pid !== null || record.processGroupId !== null || record.processStartIdentity !== null || record.launchedAt !== null))
424
+ throw new Error('prepared mutable invocation has process identity');
425
+ const adopted = record.phase === 'adopted';
426
+ if (adopted !== (typeof record.reportSha256 === 'string'
427
+ && /^[0-9a-f]{64}$/u.test(record.reportSha256) && record.resultSnapshot !== null))
428
+ throw new Error('durable mutable invocation adoption is invalid');
429
+ if (!adopted && (record.reportSha256 !== null || record.resultSnapshot !== null))
430
+ throw new Error('unfinished mutable invocation has adoption evidence');
431
+ requireReportSnapshot(record.baseline);
432
+ if (adopted)
433
+ requireReportSnapshot(record.resultSnapshot);
434
+ return structuredClone(value);
115
435
  }
116
- function validateCompletedReport(operation, attemptId, reportBytes, reviewContext) {
117
- const rawText = reportBytes.toString('utf8');
118
- const repairable = Buffer.from(rawText, 'utf8').equals(reportBytes)
119
- && !containsCredentialEvidence(rawText)
120
- && reportBytes.length <= 1024 * 1024;
121
- try {
122
- if (!Buffer.from(rawText, 'utf8').equals(reportBytes) || containsCredentialEvidence(rawText)) {
123
- throw new Error('report payload contains forbidden credential material');
124
- }
125
- const decoded = decodeAgentReportForValidation(reportBytes);
126
- let validatedPayload;
127
- let artifactSha256;
128
- if (operation === 'triage') {
129
- validatedPayload = validateTriageRoute(decoded);
130
- artifactSha256 = hashTriageArtifact(validatedPayload);
131
- }
132
- else if (operation === 'ambiguity-review') {
133
- validatedPayload = validateAmbiguityReviewArtifact(decoded);
134
- artifactSha256 = hashAmbiguityReviewArtifact(validatedPayload);
135
- }
136
- else {
137
- const reviewReport = inputReview(operation, inputReviewContext(reviewContext), decoded);
138
- validatedPayload = reviewReport;
139
- artifactSha256 = hashCodeReviewReport(reviewReport);
140
- }
141
- return {
142
- status: 'completed',
143
- attemptId,
144
- validatedPayload,
145
- artifactSha256,
146
- };
436
+ export function validateDurableReportInvocation(value) {
437
+ const keys = [
438
+ 'version', 'operation', 'attemptId', 'generationHash', 'promptFactsSha256', 'reportPath', 'phase', 'host', 'bootId', 'preparedAt',
439
+ 'launchedAt', 'pid', 'processStartIdentity', 'processGroupId', 'baseline',
440
+ ];
441
+ if (!hasExactKeys(value, keys))
442
+ throw new Error('durable report invocation is invalid');
443
+ const record = value;
444
+ if (record.version !== 1 || !['triage', 'ambiguity-review', 'code-review', 'spec-author', 'spec-review', 'acceptance-proof'].includes(record.operation)
445
+ || !nonEmpty(record.attemptId) || !/^[0-9a-f]{64}$/u.test(record.generationHash)
446
+ || !nonEmpty(record.reportPath) || !nonEmpty(record.host) || !nonEmpty(record.bootId)
447
+ || !/^[0-9a-f]{64}$/u.test(record.promptFactsSha256)
448
+ || !timestamp(record.preparedAt) || !['prepared', 'launched'].includes(record.phase))
449
+ throw new Error('durable report invocation fields are invalid');
450
+ const launched = record.phase === 'launched';
451
+ if (launched !== (positive(record.pid) && positive(record.processGroupId) && nonEmpty(record.processStartIdentity)
452
+ && timestamp(record.launchedAt)))
453
+ throw new Error('durable report invocation process identity is invalid');
454
+ if (!launched && (record.pid !== null || record.processGroupId !== null || record.processStartIdentity !== null || record.launchedAt !== null)) {
455
+ throw new Error('prepared report invocation has process identity');
147
456
  }
148
- catch (error) {
149
- return {
150
- status: 'invalid',
151
- attemptId,
152
- findings: [safeFinding(error)],
153
- ...(repairable ? { repairInput: { originalReportSha256: sha256(reportBytes), originalReportBytes: Buffer.from(reportBytes) } } : {}),
154
- };
457
+ requireReportSnapshot(record.baseline);
458
+ return structuredClone(value);
459
+ }
460
+ function hasExactReportAuthority(attempt, input) {
461
+ const policy = attempt.policy;
462
+ if (attempt.operation !== input.operation || attempt.generationHash !== input.workflowGeneration.generationHash
463
+ || !hasExactKeys(policy, POLICY_KEYS) || policy.network !== 'deny' || policy.networkHosts.length !== 0
464
+ || policy.mcpTools.length !== 0 || policy.approvalCeiling !== 'never' || policy.externalWrite !== false)
465
+ return false;
466
+ if (input.operation === 'spec-author') {
467
+ return policy.sandboxMode === 'workspace-write' && policy.cwdClass === 'target-state'
468
+ && policy.worktreeAccess === 'write' && canonicalJson(policy.writableRootClasses) === canonicalJson(['target-state'])
469
+ && policy.runnerPostcondition === 'spec-only';
470
+ }
471
+ if (input.operation === 'acceptance-proof') {
472
+ return policy.sandboxMode === 'workspace-write' && policy.cwdClass === 'worktree'
473
+ && policy.worktreeAccess === 'write' && canonicalJson(policy.writableRootClasses) === canonicalJson(['worktree'])
474
+ && policy.runnerPostcondition === 'proof-only';
155
475
  }
476
+ return policy.sandboxMode === 'read-only' && policy.cwdClass === 'worktree'
477
+ && policy.worktreeAccess === 'read-only' && Array.isArray(policy.writableRootClasses) && policy.writableRootClasses.length === 0
478
+ && policy.runnerPostcondition === 'report-only';
156
479
  }
157
- function isImplementationReview(operation) {
158
- return operation === 'code-review';
480
+ function hasExactMutableAuthority(policy) {
481
+ return hasExactKeys(policy, POLICY_KEYS) && policy.sandboxMode === 'workspace-write' && policy.cwdClass === 'worktree'
482
+ && policy.worktreeAccess === 'write' && canonicalJson(policy.writableRootClasses) === canonicalJson(['worktree'])
483
+ && policy.runnerPostcondition === 'change-set' && policy.network === 'deny' && policy.networkHosts.length === 0
484
+ && policy.mcpTools.length === 0 && policy.approvalCeiling === 'never' && policy.externalWrite === false;
159
485
  }
160
- function inputReviewContext(context) {
161
- if (!context)
162
- throw new Error('implementation review validation context is missing');
163
- return context;
486
+ function workerOperation(operation) {
487
+ return operation === 'qualification-repair' ? operation : 'implementation';
164
488
  }
165
- function inputReview(operation, context, decoded) {
166
- if (context.operation !== operation)
167
- throw new Error('implementation review operation context mismatch');
168
- return validateCodeReviewReport(decoded, context);
489
+ function completedMutable(invocation, reportBytes) {
490
+ return { status: 'completed', attemptId: invocation.attemptId, reportBytes: Buffer.from(reportBytes) };
169
491
  }
170
- function safeFinding(error) {
171
- return error instanceof Error && error.message.length > 0
172
- ? error.message.slice(0, MAX_STRING_LENGTH)
173
- : 'report payload validation failed';
492
+ function requireReportSnapshot(value) {
493
+ const keys = ['headSha', 'indexTreeSha', 'trackedContentSha256', 'untrackedContentSha256', 'worktreeIdentity'];
494
+ if (!hasExactKeys(value, keys) || !keys.every((key) => nonEmpty(value[key])))
495
+ throw new Error('report snapshot is invalid');
496
+ return structuredClone(value);
174
497
  }
175
- function sameSnapshot(left, right) {
176
- try {
177
- return canonicalJson(left) === canonicalJson(right);
178
- }
179
- catch {
180
- return false;
181
- }
498
+ function blocked(kind, code) {
499
+ return { status: 'blocked', kind, code };
182
500
  }
183
501
  function hasExactKeys(value, keys) {
184
502
  if (typeof value !== 'object' || value === null || Array.isArray(value))
@@ -187,14 +505,10 @@ function hasExactKeys(value, keys) {
187
505
  const expected = [...keys].sort();
188
506
  return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
189
507
  }
190
- function requireReportSnapshot(value) {
191
- const keys = ['headSha', 'indexTreeSha', 'trackedContentSha256', 'untrackedContentSha256', 'worktreeIdentity'];
192
- if (!hasExactKeys(value, keys))
193
- throw new Error('report-operation snapshot is invalid');
194
- for (const key of keys) {
195
- if (typeof value[key] !== 'string')
196
- throw new Error('report-operation snapshot is invalid');
197
- }
198
- return value;
508
+ function nonEmpty(value) { return typeof value === 'string' && value.length > 0; }
509
+ function positive(value) { return Number.isSafeInteger(value) && value > 0; }
510
+ function timestamp(value) { return typeof value === 'string' && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString() === value; }
511
+ function promptFactsSha256(promptFacts) { return sha256(canonicalJson(promptFacts)); }
512
+ class ReportAuthorityError extends Error {
199
513
  }
200
514
  //# sourceMappingURL=contained-report-operation.js.map