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.
- package/CHANGELOG.md +37 -0
- package/README.md +26 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/v2/acceptance-proof.d.ts +29 -16
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +159 -129
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/command.d.ts +1 -0
- package/dist/src/v2/adapters/command.d.ts.map +1 -1
- package/dist/src/v2/adapters/command.js +1 -1
- package/dist/src/v2/adapters/command.js.map +1 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.js +1 -0
- package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
- package/dist/src/v2/adapters/pull-requests.d.ts +1 -0
- package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
- package/dist/src/v2/adapters/pull-requests.js +1 -0
- package/dist/src/v2/adapters/pull-requests.js.map +1 -1
- package/dist/src/v2/adapters/worktree.js +3 -3
- package/dist/src/v2/adapters/worktree.js.map +1 -1
- package/dist/src/v2/atomic-store.d.ts +15 -0
- package/dist/src/v2/atomic-store.d.ts.map +1 -1
- package/dist/src/v2/atomic-store.js +55 -8
- package/dist/src/v2/atomic-store.js.map +1 -1
- package/dist/src/v2/candidate.d.ts +152 -0
- package/dist/src/v2/candidate.d.ts.map +1 -0
- package/dist/src/v2/candidate.js +129 -0
- package/dist/src/v2/candidate.js.map +1 -0
- package/dist/src/v2/checked-change.d.ts +37 -6
- package/dist/src/v2/checked-change.d.ts.map +1 -1
- package/dist/src/v2/checked-change.js +71 -0
- package/dist/src/v2/checked-change.js.map +1 -1
- package/dist/src/v2/cli.d.ts +6 -0
- package/dist/src/v2/cli.d.ts.map +1 -1
- package/dist/src/v2/cli.js +57 -21
- package/dist/src/v2/cli.js.map +1 -1
- package/dist/src/v2/codex-process.d.ts +1 -0
- package/dist/src/v2/codex-process.d.ts.map +1 -1
- package/dist/src/v2/codex-process.js +12 -1
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/contained-report-operation.d.ts +196 -30
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
- package/dist/src/v2/contained-report-operation.js +464 -150
- package/dist/src/v2/contained-report-operation.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +10 -28
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +44 -114
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts +4 -24
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
- package/dist/src/v2/implementation-reviewer.js +29 -35
- package/dist/src/v2/implementation-reviewer.js.map +1 -1
- package/dist/src/v2/owner-control-lock.d.ts +13 -1
- package/dist/src/v2/owner-control-lock.d.ts.map +1 -1
- package/dist/src/v2/owner-control-lock.js +66 -7
- package/dist/src/v2/owner-control-lock.js.map +1 -1
- package/dist/src/v2/proof-report.d.ts +2 -1
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +6 -40
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/proof-store.d.ts +19 -7
- package/dist/src/v2/proof-store.d.ts.map +1 -1
- package/dist/src/v2/proof-store.js +162 -41
- package/dist/src/v2/proof-store.js.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts +7 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.js +97 -70
- package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
- package/dist/src/v2/review-feedback.d.ts +0 -18
- package/dist/src/v2/review-feedback.d.ts.map +1 -1
- package/dist/src/v2/review-feedback.js +9 -34
- package/dist/src/v2/review-feedback.js.map +1 -1
- package/dist/src/v2/route-coordinator.d.ts +7 -12
- package/dist/src/v2/route-coordinator.d.ts.map +1 -1
- package/dist/src/v2/route-coordinator.js +33 -128
- package/dist/src/v2/route-coordinator.js.map +1 -1
- package/dist/src/v2/route-decision.d.ts +0 -9
- package/dist/src/v2/route-decision.d.ts.map +1 -1
- package/dist/src/v2/route-decision.js +7 -27
- package/dist/src/v2/route-decision.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +64 -26
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +1298 -620
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +39 -25
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +469 -88
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +61 -36
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +992 -402
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/spec-coordinator.d.ts +16 -28
- package/dist/src/v2/spec-coordinator.d.ts.map +1 -1
- package/dist/src/v2/spec-coordinator.js +81 -43
- package/dist/src/v2/spec-coordinator.js.map +1 -1
- package/dist/src/v2/spec-delivery.d.ts +3 -34
- package/dist/src/v2/spec-delivery.d.ts.map +1 -1
- package/dist/src/v2/spec-delivery.js +42 -107
- package/dist/src/v2/spec-delivery.js.map +1 -1
- package/docs/deep-dive.md +52 -14
- package/internal-workflow/evals/coding-skill-evals.json +58 -17
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/internal-workflow/skills/code-review/SKILL.md +18 -0
- package/internal-workflow/skills/spec-implementer/SKILL.md +8 -1
- package/package.json +1 -1
package/dist/src/v2/run-store.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { validateDurableMutableInvocation, validateDurableReportInvocation, } from './contained-report-operation.js';
|
|
2
|
+
import { projectTerminalDirectReview, validateDirectReview } from './direct-delivery.js';
|
|
2
3
|
import { validateSpecDelivery } from './spec-delivery.js';
|
|
3
4
|
import { validateRouteExecution, validateRouteReceipt, validateRouteStateInvariant, } from './route-decision.js';
|
|
4
5
|
import { validateWaitingHumanExecution } from './waiting-human.js';
|
|
5
6
|
import { posix } from 'node:path';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
6
8
|
import { AtomicStateFile } from './atomic-store.js';
|
|
7
|
-
import {
|
|
9
|
+
import { writeDurableAtomicFile } from './adapters/durable-atomic-file.js';
|
|
10
|
+
import { canonicalJson, sha256 } from './containment.js';
|
|
11
|
+
import { validateCandidateBinding, validateCandidateExecutionLease } from './candidate.js';
|
|
12
|
+
import { blockReviewFeedback, createReviewFeedbackBootstrap, validateReviewFeedbackExecution, } from './review-feedback.js';
|
|
8
13
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
9
14
|
const GIT_SHA_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
10
15
|
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
@@ -22,15 +27,132 @@ export class RouteInitializationUnrecoverableError extends Error {
|
|
|
22
27
|
}
|
|
23
28
|
export class FileRunRecordWriter {
|
|
24
29
|
file;
|
|
30
|
+
backupPath;
|
|
31
|
+
migrationMetadataPath;
|
|
32
|
+
reportLifecycleBackupPath;
|
|
33
|
+
mutableLifecycleBackupPath;
|
|
34
|
+
now;
|
|
25
35
|
constructor(path, options = {}) {
|
|
26
36
|
this.file = new AtomicStateFile(path, validateRunStateFile, options);
|
|
37
|
+
this.backupPath = `${path}.pre-candidate-v3`;
|
|
38
|
+
this.migrationMetadataPath = `${this.backupPath}.metadata.json`;
|
|
39
|
+
this.reportLifecycleBackupPath = `${path}.pre-report-lifecycle-v1`;
|
|
40
|
+
this.mutableLifecycleBackupPath = `${path}.pre-mutable-lifecycle-v1`;
|
|
41
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
27
42
|
}
|
|
28
43
|
async read() {
|
|
29
|
-
|
|
44
|
+
try {
|
|
45
|
+
return await this.file.read() ?? emptyRunState();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return this.migrateLegacyReportLifecycle(error);
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
51
|
async compareAndSwap(expectedGeneration, next) {
|
|
32
52
|
validateRunStateBody(next);
|
|
33
|
-
|
|
53
|
+
const candidate = { ...structuredClone(next), generation: expectedGeneration + 1 };
|
|
54
|
+
return this.file.compareAndSwapWithRaw(expectedGeneration, candidate, async (priorBytes) => {
|
|
55
|
+
if (!priorBytes)
|
|
56
|
+
return;
|
|
57
|
+
const raw = parseRawState(priorBytes);
|
|
58
|
+
if (raw.version === 3 && next.version !== 3)
|
|
59
|
+
throw new Error('run state V3 downgrade requires explicit rollback');
|
|
60
|
+
if (raw.version === 2 && next.version === 3)
|
|
61
|
+
await this.ensureCandidateMigrationBackup(priorBytes, raw.generation);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async markPublicationEffectPossible() {
|
|
65
|
+
await this.file.withExclusiveRaw(async () => {
|
|
66
|
+
const existing = await readMigrationMetadata(this.migrationMetadataPath);
|
|
67
|
+
if (!existing?.publicationEffectPossible) {
|
|
68
|
+
const now = this.now();
|
|
69
|
+
await writeDurableAtomicFile(this.migrationMetadataPath, `${canonicalJson({
|
|
70
|
+
version: 1,
|
|
71
|
+
sourceGeneration: existing?.sourceGeneration ?? null,
|
|
72
|
+
sourceBytesSha256: existing?.sourceBytesSha256 ?? null,
|
|
73
|
+
publicationEffectPossible: true,
|
|
74
|
+
createdAt: existing?.createdAt ?? now,
|
|
75
|
+
updatedAt: now,
|
|
76
|
+
})}\n`);
|
|
77
|
+
}
|
|
78
|
+
return { result: undefined };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async rollbackCandidateMigration(input) {
|
|
82
|
+
return this.file.withExclusiveRaw(async (_currentBytes, current) => {
|
|
83
|
+
const metadata = await readMigrationMetadata(this.migrationMetadataPath);
|
|
84
|
+
if (!metadata || metadata.sourceGeneration === null || metadata.sourceBytesSha256 === null)
|
|
85
|
+
throw new Error('candidate V3 rollback backup is unavailable');
|
|
86
|
+
if (metadata.publicationEffectPossible)
|
|
87
|
+
throw new Error('candidate V3 rollback is forbidden after publication boundary');
|
|
88
|
+
if (!current || current.version !== 3)
|
|
89
|
+
throw new Error('candidate V3 rollback requires active V3 state');
|
|
90
|
+
await input.assertNoActiveProcesses();
|
|
91
|
+
await input.cleanupCandidateState(structuredClone(current));
|
|
92
|
+
const backup = await readOptionalFile(this.backupPath);
|
|
93
|
+
if (!backup || sha256(backup) !== metadata.sourceBytesSha256)
|
|
94
|
+
throw new Error('candidate V3 rollback backup identity is invalid');
|
|
95
|
+
const restored = parseRawState(backup);
|
|
96
|
+
if (restored.version !== 2 || restored.generation !== metadata.sourceGeneration)
|
|
97
|
+
throw new Error('candidate V3 rollback backup generation is invalid');
|
|
98
|
+
return { result: restored, replacementBytes: backup };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async ensureCandidateMigrationBackup(rawV2Bytes, generation) {
|
|
102
|
+
const sourceBytesSha256 = sha256(rawV2Bytes);
|
|
103
|
+
const existing = await readMigrationMetadata(this.migrationMetadataPath);
|
|
104
|
+
const backup = await readOptionalFile(this.backupPath);
|
|
105
|
+
if (existing?.publicationEffectPossible)
|
|
106
|
+
throw new Error('candidate V3 migration backup cannot change after publication boundary');
|
|
107
|
+
if (existing?.sourceGeneration === generation && existing.sourceBytesSha256 === sourceBytesSha256
|
|
108
|
+
&& backup?.equals(rawV2Bytes))
|
|
109
|
+
return;
|
|
110
|
+
await writeDurableAtomicFile(this.backupPath, rawV2Bytes);
|
|
111
|
+
const now = this.now();
|
|
112
|
+
await writeDurableAtomicFile(this.migrationMetadataPath, `${canonicalJson({
|
|
113
|
+
version: 1,
|
|
114
|
+
sourceGeneration: generation,
|
|
115
|
+
sourceBytesSha256,
|
|
116
|
+
publicationEffectPossible: false,
|
|
117
|
+
createdAt: now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
})}\n`);
|
|
120
|
+
}
|
|
121
|
+
async migrateLegacyReportLifecycle(originalError) {
|
|
122
|
+
return this.file.withExclusiveUnparsedRaw(async (priorBytes) => {
|
|
123
|
+
if (!priorBytes)
|
|
124
|
+
return { result: emptyRunState() };
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = JSON.parse(priorBytes.toString('utf8'));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw originalError;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return { result: validateRunStateFile(raw) };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
const mutable = hasLegacyMutableLifecycle(raw);
|
|
137
|
+
let backupPath = mutable ? this.mutableLifecycleBackupPath : this.reportLifecycleBackupPath;
|
|
138
|
+
if (!mutable) {
|
|
139
|
+
const priorReportBackup = await readOptionalFile(backupPath);
|
|
140
|
+
if (priorReportBackup && !priorReportBackup.equals(priorBytes)) {
|
|
141
|
+
const generation = raw.generation;
|
|
142
|
+
backupPath = `${backupPath}.g${String(generation)}-${sha256(priorBytes).slice(0, 16)}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const migrated = canonicalizeLegacyReportLifecycle(raw, backupPath);
|
|
146
|
+
if (!migrated)
|
|
147
|
+
throw originalError;
|
|
148
|
+
const existingBackup = await readOptionalFile(backupPath);
|
|
149
|
+
if (existingBackup && !existingBackup.equals(priorBytes))
|
|
150
|
+
throw new Error('report lifecycle migration backup conflicts with current source bytes');
|
|
151
|
+
if (!existingBackup)
|
|
152
|
+
await writeDurableAtomicFile(backupPath, priorBytes);
|
|
153
|
+
return { result: migrated, replacementBytes: Buffer.from(`${canonicalJson(migrated)}\n`) };
|
|
154
|
+
}
|
|
155
|
+
});
|
|
34
156
|
}
|
|
35
157
|
}
|
|
36
158
|
export class InMemoryRunRecordWriter {
|
|
@@ -41,14 +163,17 @@ export class InMemoryRunRecordWriter {
|
|
|
41
163
|
async compareAndSwap(expectedGeneration, next) {
|
|
42
164
|
if (this.state.generation !== expectedGeneration)
|
|
43
165
|
throw new Error('run state generation conflict');
|
|
166
|
+
if (this.state.version === 3 && next.version !== 3)
|
|
167
|
+
throw new Error('run state V3 downgrade requires explicit rollback');
|
|
44
168
|
const value = validateRunStateFile({ ...structuredClone(next), generation: expectedGeneration + 1 });
|
|
45
169
|
this.state = value;
|
|
46
170
|
return structuredClone(value);
|
|
47
171
|
}
|
|
172
|
+
async markPublicationEffectPossible() { }
|
|
48
173
|
}
|
|
49
174
|
export function validateRunStateFile(value) {
|
|
50
175
|
assertExactObject(value, ['schema', 'version', 'generation', 'runs'], 'run state');
|
|
51
|
-
if (value.schema !== 'codex-orchestrator.agent-auto-state' ||
|
|
176
|
+
if (value.schema !== 'codex-orchestrator.agent-auto-state' || ![1, 2, 3].includes(value.version))
|
|
52
177
|
throw new Error('run state schema/version is invalid');
|
|
53
178
|
if (!Number.isSafeInteger(value.generation) || value.generation <= 0)
|
|
54
179
|
throw new Error('run state generation is invalid');
|
|
@@ -67,13 +192,17 @@ export function validateRunStateFile(value) {
|
|
|
67
192
|
: structuredClone(run)),
|
|
68
193
|
};
|
|
69
194
|
}
|
|
195
|
+
if (value.version === 2)
|
|
196
|
+
assertNoCandidateV3Fields(value.runs);
|
|
70
197
|
return value;
|
|
71
198
|
}
|
|
72
199
|
function validateRunStateBody(value) {
|
|
73
200
|
assertExactObject(value, ['schema', 'version', 'runs'], 'run state body');
|
|
74
|
-
if (value.schema !== 'codex-orchestrator.agent-auto-state' || value.version !== 2)
|
|
201
|
+
if (value.schema !== 'codex-orchestrator.agent-auto-state' || (value.version !== 2 && value.version !== 3))
|
|
75
202
|
throw new Error('run state schema/version is invalid');
|
|
76
203
|
validateRuns(value.runs);
|
|
204
|
+
if (value.version === 2)
|
|
205
|
+
assertNoCandidateV3Fields(value.runs);
|
|
77
206
|
}
|
|
78
207
|
function validateRuns(value) {
|
|
79
208
|
if (!Array.isArray(value) || value.length > 256)
|
|
@@ -88,7 +217,6 @@ function validateRuns(value) {
|
|
|
88
217
|
}
|
|
89
218
|
function validateRunRecord(value, field) {
|
|
90
219
|
const optional = [
|
|
91
|
-
'process',
|
|
92
220
|
'checkedChangeSha256',
|
|
93
221
|
'proofId',
|
|
94
222
|
'proofReceipt',
|
|
@@ -103,10 +231,15 @@ function validateRunRecord(value, field) {
|
|
|
103
231
|
'reviewFeedback',
|
|
104
232
|
'checkQualification',
|
|
105
233
|
'baselineChecks',
|
|
234
|
+
'changeBindingVersion',
|
|
235
|
+
'candidateBinding',
|
|
236
|
+
'executionLease',
|
|
237
|
+
'reportInvocation',
|
|
238
|
+
'mutableInvocation',
|
|
106
239
|
].filter((key) => hasOwn(value, key));
|
|
107
240
|
assertExactObject(value, [
|
|
108
241
|
'runId', 'issueNumber', 'canonicalRepository', 'baseSha', 'branchName', 'worktreePath', 'lifecycle', 'cycle',
|
|
109
|
-
'reportRepairs', '
|
|
242
|
+
'reportRepairs', 'issueSnapshot', 'frozenCriteria', 'reworkFindings',
|
|
110
243
|
'packageVersion', 'workflowGeneration', 'skillHashes', 'checks', 'createdAt', 'updatedAt', ...optional,
|
|
111
244
|
], field);
|
|
112
245
|
if (typeof value.runId !== 'string' || !UUID_V4_PATTERN.test(value.runId))
|
|
@@ -126,8 +259,6 @@ function validateRunRecord(value, field) {
|
|
|
126
259
|
throw new Error(`${field}.cycle is invalid`);
|
|
127
260
|
if (value.reportRepairs !== 0 && value.reportRepairs !== 1)
|
|
128
261
|
throw new Error(`${field}.reportRepairs is invalid`);
|
|
129
|
-
if (value.transportRetries !== 0 && value.transportRetries !== 1)
|
|
130
|
-
throw new Error(`${field}.transportRetries is invalid`);
|
|
131
262
|
validateIssueSnapshot(value.issueSnapshot, `${field}.issueSnapshot`);
|
|
132
263
|
validateFrozenCriteria(value.frozenCriteria, `${field}.frozenCriteria`);
|
|
133
264
|
validateStringList(value.reworkFindings, `${field}.reworkFindings`);
|
|
@@ -146,8 +277,32 @@ function validateRunRecord(value, field) {
|
|
|
146
277
|
if (hasOwn(value, 'baselineChecks'))
|
|
147
278
|
validateChecks(value.baselineChecks, `${field}.baselineChecks`, false);
|
|
148
279
|
validateChecks(value.checks, `${field}.checks`);
|
|
149
|
-
|
|
150
|
-
|
|
280
|
+
let reportInvocation;
|
|
281
|
+
if (hasOwn(value, 'reportInvocation')) {
|
|
282
|
+
const invocation = validateDurableReportInvocation(value.reportInvocation);
|
|
283
|
+
reportInvocation = invocation;
|
|
284
|
+
const expectedLifecycle = invocation.operation === 'triage' || invocation.operation === 'ambiguity-review'
|
|
285
|
+
? 'triaging'
|
|
286
|
+
: invocation.operation === 'code-review' ? 'implementing' : 'spec-authoring';
|
|
287
|
+
if (value.lifecycle !== expectedLifecycle)
|
|
288
|
+
throw new Error(`${field}.reportInvocation lifecycle is invalid`);
|
|
289
|
+
}
|
|
290
|
+
if (hasOwn(value, 'mutableInvocation')) {
|
|
291
|
+
const invocation = validateDurableMutableInvocation(value.mutableInvocation);
|
|
292
|
+
if (value.lifecycle !== 'implementing')
|
|
293
|
+
throw new Error(`${field}.mutableInvocation lifecycle is invalid`);
|
|
294
|
+
if (invocation.worktreePath !== value.worktreePath || invocation.generationHash !== routeGenerationHash) {
|
|
295
|
+
throw new Error(`${field}.mutableInvocation binding is invalid`);
|
|
296
|
+
}
|
|
297
|
+
if (invocation.operation === 'qualification-repair' && !hasOwn(value, 'checkQualification')) {
|
|
298
|
+
throw new Error(`${field}.qualification mutableInvocation requires check qualification`);
|
|
299
|
+
}
|
|
300
|
+
const feedbackActive = hasOwn(value, 'reviewFeedback')
|
|
301
|
+
&& value.reviewFeedback.activeBatch !== null;
|
|
302
|
+
if ((invocation.operation === 'review-feedback-implementation') !== feedbackActive) {
|
|
303
|
+
throw new Error(`${field}.mutableInvocation feedback binding is invalid`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
151
306
|
if (hasOwn(value, 'checkedChangeSha256'))
|
|
152
307
|
assertSha256(value.checkedChangeSha256, `${field}.checkedChangeSha256`);
|
|
153
308
|
if (hasOwn(value, 'proofId'))
|
|
@@ -175,24 +330,9 @@ function validateRunRecord(value, field) {
|
|
|
175
330
|
if (!hasOwn(value, 'routeReceipt') || value.routeReceipt.route !== 'direct') {
|
|
176
331
|
throw new Error(`${field}.directReview requires a direct route`);
|
|
177
332
|
}
|
|
178
|
-
const rawProcess = hasOwn(value, 'process') && hasOwn(value.process, 'purpose')
|
|
179
|
-
? value.process
|
|
180
|
-
: undefined;
|
|
181
|
-
const process = rawProcess && !['spec-author', 'spec-review'].includes(rawProcess.purpose)
|
|
182
|
-
? {
|
|
183
|
-
purpose: rawProcess.purpose,
|
|
184
|
-
resumeLifecycle: rawProcess.resumeLifecycle,
|
|
185
|
-
resumeReviewStage: rawProcess.resumeReviewStage,
|
|
186
|
-
}
|
|
187
|
-
: undefined;
|
|
188
333
|
validateDirectReview(value.directReview, {
|
|
189
334
|
lifecycle: value.lifecycle,
|
|
190
335
|
...(hasOwn(value, 'terminalOutcome') ? { terminalOutcome: directTerminalOutcome(value.terminalOutcome) } : {}),
|
|
191
|
-
...(process ? { process: {
|
|
192
|
-
purpose: process.purpose,
|
|
193
|
-
resumeLifecycle: process.resumeLifecycle,
|
|
194
|
-
resumeReviewStage: process.resumeReviewStage,
|
|
195
|
-
} } : {}),
|
|
196
336
|
});
|
|
197
337
|
}
|
|
198
338
|
if (hasOwn(value, 'specDelivery')) {
|
|
@@ -209,6 +349,31 @@ function validateRunRecord(value, field) {
|
|
|
209
349
|
validateReviewFeedbackExecution(value.reviewFeedback);
|
|
210
350
|
validateReviewFeedbackRunInvariant(value, field);
|
|
211
351
|
}
|
|
352
|
+
if (hasOwn(value, 'changeBindingVersion') && value.changeBindingVersion !== 2)
|
|
353
|
+
throw new Error(`${field}.changeBindingVersion is invalid`);
|
|
354
|
+
if (hasOwn(value, 'candidateBinding'))
|
|
355
|
+
validateCandidateBinding(value.candidateBinding, `${field}.candidateBinding`, value.runId);
|
|
356
|
+
if (hasOwn(value, 'executionLease'))
|
|
357
|
+
validateCandidateExecutionLease(value.executionLease, `${field}.executionLease`);
|
|
358
|
+
if (hasOwn(value, 'candidateBinding') !== hasOwn(value, 'changeBindingVersion'))
|
|
359
|
+
throw new Error(`${field} candidate binding version is incomplete`);
|
|
360
|
+
if (hasOwn(value, 'executionLease')) {
|
|
361
|
+
if (!hasOwn(value, 'candidateBinding'))
|
|
362
|
+
throw new Error(`${field}.executionLease requires candidate binding`);
|
|
363
|
+
const binding = value.candidateBinding;
|
|
364
|
+
const lease = value.executionLease;
|
|
365
|
+
if (lease.bindingId !== binding.bindingId || lease.candidateCommitSha !== binding.candidateCommitSha)
|
|
366
|
+
throw new Error(`${field}.executionLease binding is invalid`);
|
|
367
|
+
}
|
|
368
|
+
if (hasOwn(value, 'intent') && hasOwn(value.intent, 'candidateRef')) {
|
|
369
|
+
if (!hasOwn(value, 'candidateBinding'))
|
|
370
|
+
throw new Error(`${field}.intent candidate ref requires candidate binding`);
|
|
371
|
+
const binding = value.candidateBinding;
|
|
372
|
+
const intent = value.intent;
|
|
373
|
+
if (intent.candidateRef !== binding.candidateRef || intent.treeSha !== binding.candidateTreeSha) {
|
|
374
|
+
throw new Error(`${field}.intent candidate binding is invalid`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
212
377
|
if (hasOwn(value, 'checkQualification'))
|
|
213
378
|
validateCheckQualification(value.checkQualification, `${field}.checkQualification`);
|
|
214
379
|
assertTimestamp(value.createdAt, `${field}.createdAt`);
|
|
@@ -224,14 +389,16 @@ function validateRunRecord(value, field) {
|
|
|
224
389
|
}
|
|
225
390
|
if (value.lifecycle === 'publishing' && !hasOwn(value, 'proofReceipt'))
|
|
226
391
|
throw new Error(`${field} publishing requires proofReceipt`);
|
|
227
|
-
if (value.lifecycle === 'safe-halt' && !hasOwn(value, 'process'))
|
|
228
|
-
throw new Error(`${field} safe-halt requires retained process evidence`);
|
|
229
392
|
if (value.lifecycle === 'review-ready' && (!hasOwn(value, 'proofReceipt') || hasOwn(value, 'intent'))) {
|
|
230
393
|
throw new Error(`${field} review-ready requires proofReceipt and no intent`);
|
|
231
394
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
395
|
+
const retainedCandidateIntent = value.lifecycle === 'blocked'
|
|
396
|
+
&& value.terminalOutcome?.status === 'blocked'
|
|
397
|
+
&& value.terminalOutcome.kind === 'safety'
|
|
398
|
+
&& !value.terminalOutcome.resumable
|
|
399
|
+
&& hasOwn(value, 'candidateBinding')
|
|
400
|
+
&& (value.intent?.kind === 'commit' || value.intent?.kind === 'review-update-commit');
|
|
401
|
+
if (terminal && hasOwn(value, 'intent') && value.lifecycle !== 'transport-failed' && !retainedCandidateIntent)
|
|
235
402
|
throw new Error(`${field} terminal lifecycle cannot retain intent`);
|
|
236
403
|
if (value.lifecycle === 'transport-failed' && hasOwn(value, 'intent')
|
|
237
404
|
&& value.terminalOutcome.resumable) {
|
|
@@ -239,6 +406,8 @@ function validateRunRecord(value, field) {
|
|
|
239
406
|
}
|
|
240
407
|
if (value.lifecycle === 'waiting-human' && !hasOwn(value, 'waitingHuman'))
|
|
241
408
|
throw new Error(`${field} waiting-human lifecycle requires waitingHuman execution`);
|
|
409
|
+
if (reportInvocation)
|
|
410
|
+
validateReportInvocationBinding(value, reportInvocation, routeGenerationHash, field);
|
|
242
411
|
validateRouteStateInvariant({
|
|
243
412
|
lifecycle: value.lifecycle,
|
|
244
413
|
routeExecution: value.routeExecution,
|
|
@@ -246,6 +415,39 @@ function validateRunRecord(value, field) {
|
|
|
246
415
|
generationHash: routeGenerationHash,
|
|
247
416
|
});
|
|
248
417
|
}
|
|
418
|
+
function validateReportInvocationBinding(record, invocation, generationHash, field) {
|
|
419
|
+
if (invocation.generationHash !== generationHash)
|
|
420
|
+
throw new Error(`${field}.reportInvocation generation is invalid`);
|
|
421
|
+
if (invocation.operation === 'triage') {
|
|
422
|
+
if (!record.routeExecution || !['triage-in-flight', 'repair-in-flight'].includes(record.routeExecution.phase)) {
|
|
423
|
+
throw new Error(`${field}.triage reportInvocation phase is invalid`);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (invocation.operation === 'ambiguity-review') {
|
|
428
|
+
if (record.routeExecution?.phase !== 'review-in-flight')
|
|
429
|
+
throw new Error(`${field}.ambiguity reportInvocation phase is invalid`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (invocation.operation === 'code-review') {
|
|
433
|
+
if (record.directReview?.status !== 'active'
|
|
434
|
+
|| !['review-full', 'review-closure'].includes(record.directReview.stage ?? '')
|
|
435
|
+
|| record.executionLease?.operation !== 'direct-review') {
|
|
436
|
+
throw new Error(`${field}.code-review reportInvocation binding is invalid`);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (invocation.operation === 'spec-author') {
|
|
441
|
+
if (!record.specDelivery || !['authoring', 'author-repair'].includes(record.specDelivery.stage)
|
|
442
|
+
|| record.specDelivery.authorSessionId === null) {
|
|
443
|
+
throw new Error(`${field}.spec-author reportInvocation binding is invalid`);
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!record.specDelivery || !['review-full', 'review-closure'].includes(record.specDelivery.stage)) {
|
|
448
|
+
throw new Error(`${field}.spec-review reportInvocation binding is invalid`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
249
451
|
function directTerminalOutcome(outcome) {
|
|
250
452
|
if (outcome.status === 'review-ready')
|
|
251
453
|
return undefined;
|
|
@@ -265,54 +467,36 @@ function validateWorkflowGeneration(value, field) {
|
|
|
265
467
|
}
|
|
266
468
|
assertSha256(value.contentSha256, `${field}.contentSha256`);
|
|
267
469
|
}
|
|
268
|
-
function validateProcess(value, field) {
|
|
269
|
-
assertExactObject(value, [
|
|
270
|
-
'pid', 'processGroupId', 'startedAt', 'baseline', 'purpose', 'resumeLifecycle', 'resumeReviewStage',
|
|
271
|
-
], field);
|
|
272
|
-
assertPositiveInteger(value.pid, `${field}.pid`);
|
|
273
|
-
assertPositiveInteger(value.processGroupId, `${field}.processGroupId`);
|
|
274
|
-
assertTimestamp(value.startedAt, `${field}.startedAt`);
|
|
275
|
-
assertExactObject(value.baseline, [
|
|
276
|
-
'headSha', 'indexTreeSha', 'trackedContentSha256', 'untrackedContentSha256', 'worktreeIdentity',
|
|
277
|
-
], `${field}.baseline`);
|
|
278
|
-
assertGitSha(value.baseline.headSha, `${field}.baseline.headSha`);
|
|
279
|
-
assertGitSha(value.baseline.indexTreeSha, `${field}.baseline.indexTreeSha`);
|
|
280
|
-
assertSha256(value.baseline.trackedContentSha256, `${field}.baseline.trackedContentSha256`);
|
|
281
|
-
assertSha256(value.baseline.untrackedContentSha256, `${field}.baseline.untrackedContentSha256`);
|
|
282
|
-
assertNonEmptyString(value.baseline.worktreeIdentity, `${field}.baseline.worktreeIdentity`);
|
|
283
|
-
if (!['route', 'implementation', 'code-review', 'proof', 'spec-author', 'spec-review'].includes(value.purpose)) {
|
|
284
|
-
throw new Error(`${field}.purpose is invalid`);
|
|
285
|
-
}
|
|
286
|
-
if (!isLifecycle(value.resumeLifecycle))
|
|
287
|
-
throw new Error(`${field}.resumeLifecycle is invalid`);
|
|
288
|
-
if (value.resumeReviewStage !== null && ![
|
|
289
|
-
'review-full', 'review-repair', 'review-closure',
|
|
290
|
-
].includes(value.resumeReviewStage))
|
|
291
|
-
throw new Error(`${field}.resumeReviewStage is invalid`);
|
|
292
|
-
}
|
|
293
470
|
function validateChecks(value, field, allowUnchangedFailure = true) {
|
|
294
471
|
if (!Array.isArray(value) || value.length > 256)
|
|
295
472
|
throw new Error(`${field} is invalid`);
|
|
296
473
|
const ids = new Set();
|
|
297
474
|
for (const [index, check] of value.entries()) {
|
|
298
|
-
|
|
475
|
+
const candidate = hasOwn(check, 'bindingId') || hasOwn(check, 'candidateTreeSha') || hasOwn(check, 'checkPolicySha256');
|
|
476
|
+
assertExactObject(check, [
|
|
477
|
+
'id', 'command', 'status', 'outputSha256',
|
|
478
|
+
...(candidate ? ['bindingId', 'candidateTreeSha', 'checkPolicySha256'] : []),
|
|
479
|
+
], `${field}[${index}]`);
|
|
299
480
|
assertNonEmptyString(check.id, `${field}[${index}].id`);
|
|
300
481
|
assertNonEmptyString(check.command, `${field}[${index}].command`);
|
|
301
482
|
if (check.status !== 'passed' && check.status !== 'failed'
|
|
302
483
|
&& (check.status !== 'unchanged-failure' || !allowUnchangedFailure))
|
|
303
484
|
throw new Error(`${field}[${index}].status is invalid`);
|
|
304
485
|
assertSha256(check.outputSha256, `${field}[${index}].outputSha256`);
|
|
486
|
+
if (candidate) {
|
|
487
|
+
assertSha256(check.bindingId, `${field}[${index}].bindingId`);
|
|
488
|
+
assertGitSha(check.candidateTreeSha, `${field}[${index}].candidateTreeSha`);
|
|
489
|
+
assertSha256(check.checkPolicySha256, `${field}[${index}].checkPolicySha256`);
|
|
490
|
+
if (check.status === 'unchanged-failure')
|
|
491
|
+
throw new Error(`${field}[${index}] candidate status is invalid`);
|
|
492
|
+
}
|
|
305
493
|
if (ids.has(check.id))
|
|
306
494
|
throw new Error(`${field} IDs must be unique`);
|
|
307
495
|
ids.add(check.id);
|
|
308
496
|
}
|
|
309
497
|
}
|
|
310
498
|
function validateCheckQualification(value, field) {
|
|
311
|
-
const optional = [
|
|
312
|
-
...(hasOwn(value, 'implementationStarted') ? ['implementationStarted'] : []),
|
|
313
|
-
...(hasOwn(value, 'repairInvocation') ? ['repairInvocation'] : []),
|
|
314
|
-
...(hasOwn(value, 'deniedPathsBaseline') ? ['deniedPathsBaseline'] : []),
|
|
315
|
-
];
|
|
499
|
+
const optional = hasOwn(value, 'repairFindings') ? ['repairFindings'] : [];
|
|
316
500
|
assertExactObject(value, ['version', 'checkPolicySha256', 'repairAttempts', 'checks', ...optional], field);
|
|
317
501
|
if (value.version !== 1)
|
|
318
502
|
throw new Error(`${field}.version is invalid`);
|
|
@@ -321,26 +505,8 @@ function validateCheckQualification(value, field) {
|
|
|
321
505
|
throw new Error(`${field}.repairAttempts is invalid`);
|
|
322
506
|
}
|
|
323
507
|
validateChecks(value.checks, `${field}.checks`, false);
|
|
324
|
-
if (hasOwn(value, '
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
if (hasOwn(value, 'repairInvocation') && value.repairInvocation !== null) {
|
|
328
|
-
validateImplementationInvocation(value.repairInvocation);
|
|
329
|
-
}
|
|
330
|
-
if (hasOwn(value, 'deniedPathsBaseline'))
|
|
331
|
-
assertSha256(value.deniedPathsBaseline, `${field}.deniedPathsBaseline`);
|
|
332
|
-
if (hasOwn(value, 'deniedPathsBaseline') !== (hasOwn(value, 'repairInvocation') && value.repairInvocation !== null)) {
|
|
333
|
-
throw new Error(`${field}.deniedPathsBaseline requires a repair invocation`);
|
|
334
|
-
}
|
|
335
|
-
const repairInvocation = hasOwn(value, 'repairInvocation')
|
|
336
|
-
? value.repairInvocation
|
|
337
|
-
: undefined;
|
|
338
|
-
if (value.implementationStarted === true && repairInvocation !== null && repairInvocation !== undefined) {
|
|
339
|
-
throw new Error(`${field} cannot own qualification repair and implementation launches together`);
|
|
340
|
-
}
|
|
341
|
-
if (repairInvocation?.phase === 'launched' && value.repairAttempts === 0) {
|
|
342
|
-
throw new Error(`${field}.repairAttempts must reserve a launched repair`);
|
|
343
|
-
}
|
|
508
|
+
if (hasOwn(value, 'repairFindings'))
|
|
509
|
+
validateStringList(value.repairFindings, `${field}.repairFindings`);
|
|
344
510
|
}
|
|
345
511
|
function validateIssueSnapshot(value, field) {
|
|
346
512
|
const optional = hasOwn(value, 'comments') ? ['comments'] : [];
|
|
@@ -408,10 +574,12 @@ function validateIntent(value, field) {
|
|
|
408
574
|
validateStringArray(value.expected, `${field}.expected`);
|
|
409
575
|
}
|
|
410
576
|
else if (kind === 'commit') {
|
|
411
|
-
assertExactObject(value, ['kind', 'parentSha', 'treeSha', 'message'], field);
|
|
577
|
+
assertExactObject(value, ['kind', 'parentSha', 'treeSha', 'message', ...(hasOwn(value, 'candidateRef') ? ['candidateRef'] : [])], field);
|
|
412
578
|
assertGitSha(value.parentSha, `${field}.parentSha`);
|
|
413
579
|
assertGitSha(value.treeSha, `${field}.treeSha`);
|
|
414
580
|
assertNonEmptyString(value.message, `${field}.message`);
|
|
581
|
+
if (hasOwn(value, 'candidateRef'))
|
|
582
|
+
assertCandidateRef(value.candidateRef, `${field}.candidateRef`);
|
|
415
583
|
}
|
|
416
584
|
else if (kind === 'push') {
|
|
417
585
|
assertExactObject(value, ['kind', 'branch', 'sha'], field);
|
|
@@ -437,11 +605,13 @@ function validateIntent(value, field) {
|
|
|
437
605
|
validateStringArray(value.expected, `${field}.expected`);
|
|
438
606
|
}
|
|
439
607
|
else if (kind === 'review-update-commit') {
|
|
440
|
-
assertExactObject(value, ['kind', 'batchId', 'parentSha', 'treeSha', 'message'], field);
|
|
608
|
+
assertExactObject(value, ['kind', 'batchId', 'parentSha', 'treeSha', 'message', ...(hasOwn(value, 'candidateRef') ? ['candidateRef'] : [])], field);
|
|
441
609
|
assertSha256(value.batchId, `${field}.batchId`);
|
|
442
610
|
assertGitSha(value.parentSha, `${field}.parentSha`);
|
|
443
611
|
assertGitSha(value.treeSha, `${field}.treeSha`);
|
|
444
612
|
assertNonEmptyString(value.message, `${field}.message`);
|
|
613
|
+
if (hasOwn(value, 'candidateRef'))
|
|
614
|
+
assertCandidateRef(value.candidateRef, `${field}.candidateRef`);
|
|
445
615
|
}
|
|
446
616
|
else if (kind === 'review-update-push') {
|
|
447
617
|
assertExactObject(value, ['kind', 'batchId', 'branch', 'priorRemoteSha', 'sha', 'treeSha'], field);
|
|
@@ -563,7 +733,213 @@ function validateStringArray(value, field) {
|
|
|
563
733
|
throw new Error(`${field} must be sorted and unique`);
|
|
564
734
|
}
|
|
565
735
|
function emptyRunState() {
|
|
566
|
-
return { schema: 'codex-orchestrator.agent-auto-state', version:
|
|
736
|
+
return { schema: 'codex-orchestrator.agent-auto-state', version: 3, generation: 0, runs: [] };
|
|
737
|
+
}
|
|
738
|
+
function canonicalizeLegacyReportLifecycle(value, backupPath) {
|
|
739
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
740
|
+
return undefined;
|
|
741
|
+
const raw = value;
|
|
742
|
+
if (raw.schema !== 'codex-orchestrator.agent-auto-state' || (raw.version !== 2 && raw.version !== 3)
|
|
743
|
+
|| !Number.isSafeInteger(raw.generation) || raw.generation <= 0 || !Array.isArray(raw.runs))
|
|
744
|
+
return undefined;
|
|
745
|
+
const runs = structuredClone(raw.runs);
|
|
746
|
+
const changed = runs.map((run) => canonicalizeLegacyReportRun(run, backupPath)).some(Boolean);
|
|
747
|
+
if (!changed)
|
|
748
|
+
return undefined;
|
|
749
|
+
return validateRunStateFile({ schema: raw.schema, version: raw.version, generation: raw.generation + 1, runs });
|
|
750
|
+
}
|
|
751
|
+
function canonicalizeLegacyReportRun(run, backupPath) {
|
|
752
|
+
let changed = false, unsafe = false, routeInFlight = false;
|
|
753
|
+
const mutable = canonicalizeLegacyMutableRun(run);
|
|
754
|
+
changed ||= mutable.changed;
|
|
755
|
+
unsafe ||= mutable.unsafe;
|
|
756
|
+
const route = run.routeExecution;
|
|
757
|
+
if (route) {
|
|
758
|
+
const triageTransport = hasOwn(route, 'triageTransportRetries'), ambiguityTransport = hasOwn(route, 'ambiguityTransportRetries');
|
|
759
|
+
if (triageTransport !== ambiguityTransport)
|
|
760
|
+
throw new Error('legacy route transport budgets are incomplete');
|
|
761
|
+
if (triageTransport) {
|
|
762
|
+
legacyBits([route.triageTransportRetries, route.ambiguityTransportRetries], 'route');
|
|
763
|
+
delete route.triageTransportRetries;
|
|
764
|
+
delete route.ambiguityTransportRetries;
|
|
765
|
+
changed = true;
|
|
766
|
+
}
|
|
767
|
+
if (hasOwn(route, 'previousAttemptId')) {
|
|
768
|
+
delete route.previousAttemptId;
|
|
769
|
+
changed = true;
|
|
770
|
+
}
|
|
771
|
+
routeInFlight = ['triage-in-flight', 'review-in-flight', 'repair-in-flight'].includes(route.phase);
|
|
772
|
+
if (routeInFlight) {
|
|
773
|
+
unsafe = true;
|
|
774
|
+
delete route.attemptId;
|
|
775
|
+
delete route.startedAt;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const direct = run.directReview;
|
|
779
|
+
if (direct?.review && hasOwn(direct.review, 'transportRetries')) {
|
|
780
|
+
legacyBits([direct.review.transportRetries], 'direct review');
|
|
781
|
+
delete direct.review.transportRetries;
|
|
782
|
+
changed = true;
|
|
783
|
+
}
|
|
784
|
+
if (direct && hasOwn(direct, 'invocation')) {
|
|
785
|
+
delete direct.invocation;
|
|
786
|
+
unsafe = changed = true;
|
|
787
|
+
}
|
|
788
|
+
const spec = run.specDelivery;
|
|
789
|
+
if (spec?.budgets?.author && hasOwn(spec.budgets.author, 'transportRetries')) {
|
|
790
|
+
legacyBits([spec.budgets.author.transportRetries], 'spec author');
|
|
791
|
+
delete spec.budgets.author.transportRetries;
|
|
792
|
+
changed = true;
|
|
793
|
+
}
|
|
794
|
+
if (spec?.budgets?.review && hasOwn(spec.budgets.review, 'transportRetries')) {
|
|
795
|
+
legacyBits([spec.budgets.review.transportRetries], 'spec review');
|
|
796
|
+
delete spec.budgets.review.transportRetries;
|
|
797
|
+
changed = true;
|
|
798
|
+
}
|
|
799
|
+
if (spec?.review && !hasOwn(spec.review, 'reviewerSessionId')) {
|
|
800
|
+
spec.review.reviewerSessionId = spec.review.reviewer?.sessionId ?? (spec.invocation?.purpose === 'review' ? spec.invocation.sessionId : null);
|
|
801
|
+
changed = true;
|
|
802
|
+
}
|
|
803
|
+
if (spec?.invocation?.purpose === 'review') {
|
|
804
|
+
delete spec.invocation;
|
|
805
|
+
unsafe = changed = true;
|
|
806
|
+
}
|
|
807
|
+
if (spec?.invocation?.purpose === 'author') {
|
|
808
|
+
if (spec.invocation.status === 'launched')
|
|
809
|
+
unsafe = true;
|
|
810
|
+
if (spec.authorSessionId === null)
|
|
811
|
+
spec.authorSessionId = spec.invocation.sessionId;
|
|
812
|
+
else if (spec.authorSessionId !== spec.invocation.sessionId)
|
|
813
|
+
unsafe = true;
|
|
814
|
+
delete spec.invocation;
|
|
815
|
+
changed = true;
|
|
816
|
+
}
|
|
817
|
+
if (run.reportInvocation && !hasOwn(run.reportInvocation, 'promptFactsSha256')) {
|
|
818
|
+
delete run.reportInvocation;
|
|
819
|
+
unsafe = changed = true;
|
|
820
|
+
}
|
|
821
|
+
if (run.process && ['route', 'code-review', 'spec-author', 'spec-review', 'proof'].includes(run.process.purpose)) {
|
|
822
|
+
delete run.process;
|
|
823
|
+
unsafe = changed = true;
|
|
824
|
+
}
|
|
825
|
+
if (!unsafe)
|
|
826
|
+
return changed;
|
|
827
|
+
if (routeInFlight)
|
|
828
|
+
delete run.routeExecution;
|
|
829
|
+
delete run.reportInvocation;
|
|
830
|
+
delete run.process;
|
|
831
|
+
delete run.intent;
|
|
832
|
+
if (direct)
|
|
833
|
+
run.directReview = projectTerminalDirectReview(direct, { status: 'blocked', kind: 'safety' }, 'report-lifecycle-migration-identity-unavailable');
|
|
834
|
+
run.lifecycle = 'blocked';
|
|
835
|
+
run.outcomeEvidenceId = `report-lifecycle-migration:${String(run.runId)}`;
|
|
836
|
+
run.terminalOutcome = { status: 'blocked', kind: 'safety', resumable: false, evidencePath: backupPath };
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
function hasLegacyMutableLifecycle(value) {
|
|
840
|
+
if (typeof value !== 'object' || value === null || !Array.isArray(value.runs))
|
|
841
|
+
return false;
|
|
842
|
+
return (value.runs).some((run) => hasOwn(run, 'transportRetries') || run.process?.purpose === 'implementation'
|
|
843
|
+
|| hasOwn(run.checkQualification, 'implementationStarted') || hasOwn(run.checkQualification, 'repairInvocation')
|
|
844
|
+
|| hasOwn(run.checkQualification, 'deniedPathsBaseline') || hasOwn(run.reviewFeedback, 'implementationInvocation'));
|
|
845
|
+
}
|
|
846
|
+
function canonicalizeLegacyMutableRun(run) {
|
|
847
|
+
let changed = false, unsafe = false;
|
|
848
|
+
if (hasOwn(run, 'transportRetries')) {
|
|
849
|
+
legacyBits([run.transportRetries], 'mutable invocation');
|
|
850
|
+
delete run.transportRetries;
|
|
851
|
+
changed = true;
|
|
852
|
+
}
|
|
853
|
+
const qualification = run.checkQualification;
|
|
854
|
+
if (qualification) {
|
|
855
|
+
if (qualification.repairInvocation?.phase === 'launched')
|
|
856
|
+
unsafe = true;
|
|
857
|
+
for (const field of ['implementationStarted', 'repairInvocation', 'deniedPathsBaseline'])
|
|
858
|
+
if (hasOwn(qualification, field)) {
|
|
859
|
+
delete qualification[field];
|
|
860
|
+
changed = true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const feedback = run.reviewFeedback;
|
|
864
|
+
if (feedback && hasOwn(feedback, 'implementationInvocation')) {
|
|
865
|
+
if (feedback.implementationInvocation?.phase === 'launched')
|
|
866
|
+
unsafe = true;
|
|
867
|
+
delete feedback.implementationInvocation;
|
|
868
|
+
changed = true;
|
|
869
|
+
}
|
|
870
|
+
if (run.process?.purpose === 'implementation') {
|
|
871
|
+
delete run.process;
|
|
872
|
+
changed = unsafe = true;
|
|
873
|
+
}
|
|
874
|
+
if (!unsafe)
|
|
875
|
+
return { changed, unsafe };
|
|
876
|
+
delete run.mutableInvocation;
|
|
877
|
+
if (feedback?.activeBatch)
|
|
878
|
+
run.reviewFeedback = blockReviewFeedback(feedback, 'safety', String(run.updatedAt));
|
|
879
|
+
return { changed: true, unsafe: true };
|
|
880
|
+
}
|
|
881
|
+
function legacyBits(values, owner) {
|
|
882
|
+
if (values.some((value) => value !== 0 && value !== 1))
|
|
883
|
+
throw new Error(`legacy ${owner} transport budget is invalid`);
|
|
884
|
+
}
|
|
885
|
+
function parseRawState(bytes) {
|
|
886
|
+
let value;
|
|
887
|
+
try {
|
|
888
|
+
value = JSON.parse(bytes.toString('utf8'));
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
throw new Error('run state JSON is malformed');
|
|
892
|
+
}
|
|
893
|
+
return validateRunStateFile(value);
|
|
894
|
+
}
|
|
895
|
+
async function readOptionalFile(path) {
|
|
896
|
+
try {
|
|
897
|
+
return await readFile(path);
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
if (isErrorCode(error, 'ENOENT'))
|
|
901
|
+
return undefined;
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async function readMigrationMetadata(path) {
|
|
906
|
+
const bytes = await readOptionalFile(path);
|
|
907
|
+
if (!bytes)
|
|
908
|
+
return undefined;
|
|
909
|
+
let value;
|
|
910
|
+
try {
|
|
911
|
+
value = JSON.parse(bytes.toString('utf8'));
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
throw new Error('candidate migration metadata is malformed');
|
|
915
|
+
}
|
|
916
|
+
assertExactObject(value, [
|
|
917
|
+
'version', 'sourceGeneration', 'sourceBytesSha256', 'publicationEffectPossible', 'createdAt', 'updatedAt',
|
|
918
|
+
], 'candidate migration metadata');
|
|
919
|
+
if (value.version !== 1)
|
|
920
|
+
throw new Error('candidate migration metadata version is invalid');
|
|
921
|
+
if (value.sourceGeneration !== null && (!Number.isSafeInteger(value.sourceGeneration) || value.sourceGeneration <= 0)) {
|
|
922
|
+
throw new Error('candidate migration source generation is invalid');
|
|
923
|
+
}
|
|
924
|
+
if (value.sourceBytesSha256 !== null)
|
|
925
|
+
assertSha256(value.sourceBytesSha256, 'candidate migration source hash');
|
|
926
|
+
if ((value.sourceGeneration === null) !== (value.sourceBytesSha256 === null))
|
|
927
|
+
throw new Error('candidate migration source identity is incomplete');
|
|
928
|
+
if (typeof value.publicationEffectPossible !== 'boolean')
|
|
929
|
+
throw new Error('candidate migration publication watermark is invalid');
|
|
930
|
+
assertTimestamp(value.createdAt, 'candidate migration createdAt');
|
|
931
|
+
assertTimestamp(value.updatedAt, 'candidate migration updatedAt');
|
|
932
|
+
return value;
|
|
933
|
+
}
|
|
934
|
+
function assertNoCandidateV3Fields(runs) {
|
|
935
|
+
for (const run of runs) {
|
|
936
|
+
if (run.changeBindingVersion !== undefined || run.candidateBinding !== undefined || run.executionLease !== undefined) {
|
|
937
|
+
throw new Error('V2 run state cannot contain candidate V3 fields');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function isErrorCode(error, code) {
|
|
942
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
567
943
|
}
|
|
568
944
|
function validateReviewFeedbackRunInvariant(run, field) {
|
|
569
945
|
const feedback = run.reviewFeedback;
|
|
@@ -579,7 +955,7 @@ function validateReviewFeedbackRunInvariant(run, field) {
|
|
|
579
955
|
&& run.lifecycle !== 'review-ready' && !retainedQuiescentHistory) {
|
|
580
956
|
throw new Error(`${field}.reviewFeedback quiescent phase requires review-ready lifecycle`);
|
|
581
957
|
}
|
|
582
|
-
if (['frozen', 'repairing'].includes(feedback.phase) && !['implementing', 'reworking', 'checking', 'proving'
|
|
958
|
+
if (['frozen', 'repairing'].includes(feedback.phase) && !['implementing', 'reworking', 'checking', 'proving'].includes(run.lifecycle)) {
|
|
583
959
|
throw new Error(`${field}.reviewFeedback active repair phase has invalid lifecycle`);
|
|
584
960
|
}
|
|
585
961
|
if (feedback.phase === 'verified' && run.lifecycle !== 'publishing') {
|
|
@@ -606,7 +982,7 @@ function validateReviewFeedbackRunInvariant(run, field) {
|
|
|
606
982
|
}
|
|
607
983
|
function isLifecycle(value) {
|
|
608
984
|
return typeof value === 'string' && [
|
|
609
|
-
'claimed', 'triaging', 'routed', 'waiting-human', 'spec-authoring', 'implementing', 'reworking', 'checking', 'proving', 'publishing',
|
|
985
|
+
'claimed', 'triaging', 'routed', 'waiting-human', 'spec-authoring', 'implementing', 'reworking', 'checking', 'proving', 'publishing',
|
|
610
986
|
'review-ready', 'blocked', 'transport-failed', 'cancelled', 'internal-error',
|
|
611
987
|
].includes(value);
|
|
612
988
|
}
|
|
@@ -637,6 +1013,11 @@ function assertGitSha(value, field) {
|
|
|
637
1013
|
if (typeof value !== 'string' || !GIT_SHA_PATTERN.test(value))
|
|
638
1014
|
throw new Error(`${field} must be a Git object ID`);
|
|
639
1015
|
}
|
|
1016
|
+
function assertCandidateRef(value, field) {
|
|
1017
|
+
if (typeof value !== 'string' || !/^refs\/codex-orchestrator\/candidates\/[0-9a-f-]{36}\/[0-9a-f]{64}$/u.test(value)) {
|
|
1018
|
+
throw new Error(`${field} is invalid`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
640
1021
|
function assertTimestamp(value, field) {
|
|
641
1022
|
if (typeof value !== 'string' || Number.isNaN(Date.parse(value)) || new Date(value).toISOString() !== value)
|
|
642
1023
|
throw new Error(`${field} is invalid`);
|