taskchef 5.12.0 → 6.0.0

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.
@@ -3,448 +3,138 @@
3
3
  import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { normalizeCodexThreadId } from "../src/delegation.js";
6
7
 
7
- const BENCHMARK_NAME = "taskchef-delegate-e2e";
8
- const STAGE_NAMES = [
9
- "prepare-and-list-projects",
10
- "create-thread",
11
- "record-task",
12
- "resolve-provisional",
13
- ];
14
- const STAGE_OUTCOMES = Object.freeze({
15
- "prepare-and-list-projects": new Set(["success", "failed"]),
16
- "create-thread": new Set(["durable", "provisional", "failed"]),
17
- "record-task": new Set(["recorded", "failed"]),
18
- "resolve-provisional": new Set(["native", "discovered", "unresolved", "failed"]),
19
- });
20
- const RESOLUTIONS = new Set(["immediate", "native", "discovered", "unresolved"]);
21
- const RESULT_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z-taskchef-delegate-e2e\.json$/;
22
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
23
- const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
8
+ const BENCHMARK_NAME = "taskchef-executor-self-link-e2e";
9
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10
+ const RESULT_FILENAME = new RegExp(
11
+ `^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\.\\d{3}Z-${BENCHMARK_NAME}\\.json$`,
12
+ );
24
13
 
25
- function requireExactKeys(value, allowed, name) {
26
- const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
27
- if (unknown.length > 0) throw new Error(`${name} has unknown field ${unknown[0]}`);
14
+ function managedResultFilename(name) {
15
+ if (!RESULT_FILENAME.test(name)) return false;
16
+ const timestampPart = name.slice(0, -`-${BENCHMARK_NAME}.json`.length);
17
+ const iso = `${timestampPart.slice(0, 13)}:${timestampPart.slice(14, 16)}:${timestampPart.slice(17)}`;
18
+ try {
19
+ timestamp(iso, "result filename timestamp");
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
28
24
  }
29
25
 
30
- function requireObject(value, name) {
31
- if (!value || typeof value !== "object" || Array.isArray(value)) {
32
- throw new Error(`${name} must be an object`);
33
- }
26
+ function object(value, name) {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
34
28
  return value;
35
29
  }
36
30
 
37
- function requireString(value, name) {
38
- if (typeof value !== "string" || value.trim().length === 0) {
39
- throw new Error(`${name} must be a non-empty string`);
40
- }
31
+ function exact(value, keys, name) {
32
+ object(value, name);
33
+ const expected = new Set(keys);
34
+ const unexpected = Object.keys(value).find((key) => !expected.has(key));
35
+ if (unexpected) throw new Error(`${name} has unsupported field: ${unexpected}`);
36
+ const missing = keys.find((key) => !(key in value));
37
+ if (missing) throw new Error(`${name} is missing field: ${missing}`);
38
+ return value;
39
+ }
40
+
41
+ function string(value, name) {
42
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} must be a non-empty string`);
41
43
  return value;
42
44
  }
43
45
 
44
46
  function timestamp(value, name) {
45
- requireString(value, name);
46
- if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) {
47
- throw new Error(`${name} must use a four-digit UTC year`);
48
- }
47
+ string(value, name);
49
48
  const milliseconds = Date.parse(value);
50
- if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString() !== value) {
51
- throw new Error(`${name} must be a canonical UTC ISO timestamp`);
49
+ if (
50
+ !/^\d{4}-\d{2}-\d{2}T.*Z$/.test(value)
51
+ || Number.isNaN(milliseconds)
52
+ || new Date(milliseconds).toISOString() !== value
53
+ ) {
54
+ throw new Error(`${name} must be a canonical four-digit-year UTC timestamp`);
52
55
  }
53
56
  return milliseconds;
54
57
  }
55
58
 
56
- function nullableString(value, name) {
57
- if (value === null) return null;
58
- return requireString(value, name);
59
- }
60
-
61
- function managedResultFilename(name) {
62
- if (!RESULT_FILENAME_PATTERN.test(name)) return false;
63
- const timestampPart = name.slice(0, -`-${BENCHMARK_NAME}.json`.length);
64
- const iso = `${timestampPart.slice(0, 13)}:${timestampPart.slice(14, 16)}:${timestampPart.slice(17)}`;
65
- try {
66
- timestamp(iso, "result filename timestamp");
67
- return true;
68
- } catch {
69
- return false;
70
- }
71
- }
72
-
73
- function requireNonnegativeNumber(value, name) {
74
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
75
- throw new Error(`${name} must be a nonnegative number`);
76
- }
59
+ function uuid(value, name) {
60
+ string(value, name);
61
+ if (!UUID_PATTERN.test(value)) throw new Error(`${name} must be a lowercase full UUID`);
77
62
  return value;
78
63
  }
79
64
 
80
- function normalizeObservations(
81
- input,
82
- startedMs,
83
- completedMs,
84
- preparationStage,
85
- creationStage,
86
- recordingStage,
87
- resolutionStage,
88
- resolutionAttempts,
89
- ) {
90
- if (input === undefined) return undefined;
91
- const observations = requireObject(input, "observations");
92
- requireExactKeys(observations, [
93
- "preparation",
94
- "resolutionSnapshots",
95
- "validationDurationMs",
96
- "candidateMetadata",
97
- ], "observations");
98
- const result = {};
99
- if (observations.preparation !== undefined) {
100
- const preparation = requireObject(observations.preparation, "observations.preparation");
101
- requireExactKeys(preparation, ["dispatchPrepareMs", "nativeProjectListMs"], "observations.preparation");
102
- result.preparation = {
103
- dispatchPrepareMs: requireNonnegativeNumber(preparation.dispatchPrepareMs, "observations.preparation.dispatchPrepareMs"),
104
- nativeProjectListMs: requireNonnegativeNumber(preparation.nativeProjectListMs, "observations.preparation.nativeProjectListMs"),
105
- };
106
- const preparationDurationMs = timestamp(preparationStage.completedAt, "prepare-and-list-projects.completedAt") -
107
- timestamp(preparationStage.startedAt, "prepare-and-list-projects.startedAt");
108
- if (Object.values(result.preparation).some((durationMs) => durationMs > preparationDurationMs)) {
109
- throw new Error("observations.preparation durations must fit within the preparation stage");
110
- }
111
- }
112
- if (observations.resolutionSnapshots !== undefined) {
113
- if (!Array.isArray(observations.resolutionSnapshots)) {
114
- throw new Error("observations.resolutionSnapshots must be an array");
115
- }
116
- let previousSnapshotStartedMs = null;
117
- let previousSnapshotCompletedMs = null;
118
- result.resolutionSnapshots = observations.resolutionSnapshots.map((snapshot, index) => {
119
- requireObject(snapshot, `observations.resolutionSnapshots[${index}]`);
120
- requireExactKeys(snapshot, [
121
- "attempt", "startedAt", "completedAt", "recentTaskCount", "candidateCount", "exactMatchCount",
122
- "resolveWriteMs", "resolveWriteOutcome",
123
- ], `observations.resolutionSnapshots[${index}]`);
124
- if (!Number.isInteger(snapshot.attempt) || snapshot.attempt !== index + 1 || snapshot.attempt > resolutionAttempts) {
125
- throw new Error(`observations.resolutionSnapshots[${index}].attempt must be sequential and within resolutionAttempts`);
126
- }
127
- const snapshotStartedMs = timestamp(snapshot.startedAt, `observations.resolutionSnapshots[${index}].startedAt`);
128
- const snapshotCompletedMs = timestamp(snapshot.completedAt, `observations.resolutionSnapshots[${index}].completedAt`);
129
- const resolutionStartedMs = resolutionStage ? timestamp(resolutionStage.startedAt, "resolve-provisional.startedAt") : null;
130
- const resolutionCompletedMs = resolutionStage ? timestamp(resolutionStage.completedAt, "resolve-provisional.completedAt") : null;
131
- if (
132
- resolutionStartedMs === null || snapshotStartedMs < resolutionStartedMs ||
133
- snapshotCompletedMs < snapshotStartedMs || snapshotCompletedMs > resolutionCompletedMs ||
134
- snapshotStartedMs < startedMs || snapshotCompletedMs > completedMs
135
- ) {
136
- throw new Error(`observations.resolutionSnapshots[${index}] must fall within the resolution stage`);
137
- }
138
- const firstCheckpointMs = Math.max(
139
- timestamp(creationStage.completedAt, "create-thread.completedAt") + 10_000,
140
- timestamp(recordingStage.completedAt, "record-task.completedAt"),
141
- );
142
- if (index === 0 && snapshotStartedMs < firstCheckpointMs) {
143
- throw new Error("the first resolution snapshot must respect the 10-second catch-up checkpoint");
144
- }
145
- if (previousSnapshotCompletedMs !== null && snapshotStartedMs < previousSnapshotCompletedMs) {
146
- throw new Error("resolutionSnapshots must be ordered and non-overlapping");
147
- }
148
- if (previousSnapshotStartedMs !== null && snapshotStartedMs - previousSnapshotStartedMs < 20_000) {
149
- throw new Error("resolutionSnapshots must start at least 20 seconds apart");
150
- }
151
- for (const name of ["recentTaskCount", "candidateCount", "exactMatchCount", "resolveWriteMs"]) {
152
- requireNonnegativeNumber(snapshot[name], `observations.resolutionSnapshots[${index}].${name}`);
153
- }
154
- if (!["not-attempted", "succeeded", "failed"].includes(snapshot.resolveWriteOutcome)) {
155
- throw new Error(`observations.resolutionSnapshots[${index}].resolveWriteOutcome is invalid`);
156
- }
157
- if ((snapshot.exactMatchCount === 1) !== (snapshot.resolveWriteOutcome !== "not-attempted")) {
158
- throw new Error(`observations.resolutionSnapshots[${index}] resolve write must agree with exact matches`);
159
- }
160
- if (snapshot.resolveWriteOutcome === "not-attempted" && snapshot.resolveWriteMs !== 0) {
161
- throw new Error(`observations.resolutionSnapshots[${index}].resolveWriteMs must be zero when no write was attempted`);
162
- }
163
- if (!Number.isInteger(snapshot.recentTaskCount) || !Number.isInteger(snapshot.candidateCount) || !Number.isInteger(snapshot.exactMatchCount)) {
164
- throw new Error(`observations.resolutionSnapshots[${index}] counts must be integers`);
165
- }
166
- if (snapshot.recentTaskCount > 50) {
167
- throw new Error(`observations.resolutionSnapshots[${index}].recentTaskCount must not exceed 50`);
168
- }
169
- if (snapshot.candidateCount > snapshot.recentTaskCount || snapshot.exactMatchCount > snapshot.candidateCount) {
170
- throw new Error(`observations.resolutionSnapshots[${index}] counts are inconsistent`);
171
- }
172
- if (snapshot.resolveWriteMs > snapshotCompletedMs - snapshotStartedMs) {
173
- throw new Error(`observations.resolutionSnapshots[${index}].resolveWriteMs exceeds snapshot duration`);
174
- }
175
- previousSnapshotStartedMs = snapshotStartedMs;
176
- previousSnapshotCompletedMs = snapshotCompletedMs;
177
- return { ...snapshot };
178
- });
179
- }
180
- if (observations.validationDurationMs !== undefined) {
181
- result.validationDurationMs = requireNonnegativeNumber(observations.validationDurationMs, "observations.validationDurationMs");
182
- const finalStage = resolutionStage ?? recordingStage ?? creationStage ?? preparationStage;
183
- const postWorkflowMs = completedMs - timestamp(finalStage.completedAt, `${finalStage.name}.completedAt`);
184
- if (result.validationDurationMs > postWorkflowMs) {
185
- throw new Error("observations.validationDurationMs must fit after the workflow stages");
186
- }
187
- }
188
- if (observations.candidateMetadata !== undefined) {
189
- const metadata = requireObject(observations.candidateMetadata, "observations.candidateMetadata");
190
- const names = ["targetProjectIdPresent", "targetCreatedAtPresent", "targetEnvironmentPresent", "targetCwdPresent"];
191
- requireExactKeys(metadata, names, "observations.candidateMetadata");
192
- for (const name of names) {
193
- if (typeof metadata[name] !== "boolean") throw new Error(`observations.candidateMetadata.${name} must be boolean`);
194
- }
195
- result.candidateMetadata = { ...metadata };
196
- }
197
- return result;
65
+ function codexUuid(value, name) {
66
+ return normalizeCodexThreadId(value, name);
198
67
  }
199
68
 
200
69
  export function normalizeBenchmarkResult(input, { requireDerived = false } = {}) {
201
- const value = structuredClone(requireObject(input, "benchmark result"));
202
- requireExactKeys(value, [
70
+ const value = structuredClone(object(input, "benchmark result"));
71
+ if (!("summary" in value) && !requireDerived) value.summary = undefined;
72
+ exact(value, [
203
73
  "schemaVersion", "benchmark", "taskchefVersion", "runId", "startedAt", "completedAt",
204
- "workload", "task", "stages", "validation", "observations", "summary",
74
+ "workload", "task", "turns", "events", "validation", "summary",
205
75
  ], "benchmark result");
206
- if (value.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
207
- if (value.benchmark !== BENCHMARK_NAME) {
208
- throw new Error(`benchmark must be ${BENCHMARK_NAME}`);
209
- }
210
- requireString(value.taskchefVersion, "taskchefVersion");
211
- if (!SEMVER_PATTERN.test(value.taskchefVersion)) throw new Error("taskchefVersion must be a semantic version");
212
- requireString(value.runId, "runId");
213
- const startedMs = timestamp(value.startedAt, "startedAt");
214
- const completedMs = timestamp(value.completedAt, "completedAt");
215
- if (completedMs < startedMs) throw new Error("completedAt must not precede startedAt");
76
+ if (value.schemaVersion !== 2) throw new Error("schemaVersion must be 2");
77
+ if (value.benchmark !== BENCHMARK_NAME) throw new Error(`benchmark must be ${BENCHMARK_NAME}`);
78
+ string(value.taskchefVersion, "taskchefVersion");
79
+ string(value.runId, "runId");
80
+ const started = timestamp(value.startedAt, "startedAt");
81
+ const completed = timestamp(value.completedAt, "completedAt");
82
+ if (completed < started) throw new Error("completedAt must not precede startedAt");
83
+
84
+ exact(value.workload, ["project", "title", "prompt"], "workload");
85
+ for (const key of ["project", "title", "prompt"]) string(value.workload[key], `workload.${key}`);
86
+ if (/<!--\s*taskchef_id=/i.test(value.workload.prompt)) throw new Error("workload.prompt must not contain a TaskChef marker");
87
+
88
+ exact(value.task, ["taskId", "threadId", "clientThreadId", "recorded", "linked"], "task");
89
+ uuid(value.task.taskId, "task.taskId");
90
+ value.task.threadId = codexUuid(value.task.threadId, "task.threadId");
91
+ if (value.task.clientThreadId !== null) {
92
+ string(value.task.clientThreadId, "task.clientThreadId");
93
+ if (!value.task.clientThreadId.startsWith("local:")) throw new Error("task.clientThreadId must be provisional");
94
+ }
95
+ for (const key of ["recorded", "linked"]) {
96
+ if (typeof value.task[key] !== "boolean") throw new Error(`task.${key} must be boolean`);
97
+ }
98
+ if (!value.task.recorded || !value.task.linked) throw new Error("benchmark task must be recorded and self-linked");
99
+
100
+ exact(value.turns, ["needsInput", "completion"], "turns");
101
+ value.turns.needsInput = codexUuid(value.turns.needsInput, "turns.needsInput");
102
+ value.turns.completion = codexUuid(value.turns.completion, "turns.completion");
103
+ if (value.turns.completion <= value.turns.needsInput) {
104
+ throw new Error("follow-up completion must use a newer turn ID");
105
+ }
106
+
107
+ const eventNames = ["preparedAt", "recordedAt", "createdAt", "linkedAt", "needsInputAt", "followedUpAt", "completedAt"];
108
+ exact(value.events, eventNames, "events");
109
+ let previous = started;
110
+ for (const name of eventNames) {
111
+ const current = timestamp(value.events[name], `events.${name}`);
112
+ if (current < previous || current > completed) throw new Error("events must be ordered within the benchmark interval");
113
+ previous = current;
114
+ }
115
+ if (Date.parse(value.events.recordedAt) > Date.parse(value.events.createdAt)) throw new Error("record must precede native creation");
216
116
 
217
- const workload = requireObject(value.workload, "workload");
218
- requireExactKeys(workload, ["project", "title", "prompt"], "workload");
219
- requireString(workload.project, "workload.project");
220
- requireString(workload.title, "workload.title");
221
- requireString(workload.prompt, "workload.prompt");
222
- if (/<!--\s*taskchef_id=/i.test(workload.prompt)) {
223
- throw new Error("workload.prompt must not contain a TaskChef marker");
224
- }
225
-
226
- const task = requireObject(value.task, "task");
227
- requireExactKeys(task, [
228
- "taskId", "threadId", "clientThreadId", "recorded", "resolution", "resolutionAttempts",
229
- ], "task");
230
- task.taskId = nullableString(task.taskId, "task.taskId");
231
- if (task.taskId !== null && !UUID_PATTERN.test(task.taskId)) {
232
- throw new Error("task.taskId must be a lowercase UUID");
233
- }
234
- task.threadId = nullableString(task.threadId, "task.threadId");
235
- task.clientThreadId = nullableString(task.clientThreadId, "task.clientThreadId");
236
- if (task.threadId !== null && task.threadId.trim().toLowerCase().startsWith("local:")) {
237
- throw new Error("task.threadId must not use the provisional local: namespace");
238
- }
239
- if (task.threadId !== null && task.threadId === task.clientThreadId) {
240
- throw new Error("task.threadId must differ from task.clientThreadId");
241
- }
242
- if (typeof task.recorded !== "boolean") throw new Error("task.recorded must be boolean");
243
- requireString(task.resolution, "task.resolution");
244
- if (!RESOLUTIONS.has(task.resolution)) throw new Error("task.resolution is invalid");
245
- if (!Number.isInteger(task.resolutionAttempts) || task.resolutionAttempts < 0) {
246
- throw new Error("task.resolutionAttempts must be a nonnegative integer");
247
- }
248
- if (task.resolutionAttempts > 2) throw new Error("task.resolutionAttempts must not exceed 2");
249
-
250
- if (!Array.isArray(value.stages)) throw new Error("stages must be an array");
251
- const stagesByName = new Map();
252
- let previousCompletedMs = startedMs;
253
- for (const [index, stage] of value.stages.entries()) {
254
- requireObject(stage, `stages[${index}]`);
255
- requireExactKeys(stage, ["name", "startedAt", "completedAt", "outcome", "durationMs"], `stages[${index}]`);
256
- requireString(stage.name, `stages[${index}].name`);
257
- if (!STAGE_NAMES.includes(stage.name)) throw new Error(`unknown stage ${stage.name}`);
258
- if (stagesByName.has(stage.name)) throw new Error(`duplicate stage ${stage.name}`);
259
- const stageStartedMs = timestamp(stage.startedAt, `${stage.name}.startedAt`);
260
- const stageCompletedMs = timestamp(stage.completedAt, `${stage.name}.completedAt`);
261
- if (stageCompletedMs < stageStartedMs) {
262
- throw new Error(`${stage.name}.completedAt must not precede startedAt`);
263
- }
264
- if (stageStartedMs < startedMs || stageCompletedMs > completedMs) {
265
- throw new Error(`${stage.name} must fall within the benchmark run`);
266
- }
267
- if (stageStartedMs < previousCompletedMs) throw new Error("stages must be ordered and non-overlapping");
268
- requireString(stage.outcome, `${stage.name}.outcome`);
269
- if (!STAGE_OUTCOMES[stage.name].has(stage.outcome)) {
270
- throw new Error(`${stage.name}.outcome is invalid`);
271
- }
272
- const durationMs = stageCompletedMs - stageStartedMs;
273
- if (requireDerived && stage.durationMs === undefined) {
274
- throw new Error(`${stage.name}.durationMs is required in a saved result`);
275
- }
276
- if (stage.durationMs !== undefined && stage.durationMs !== durationMs) {
277
- throw new Error(`${stage.name}.durationMs does not match its timestamps`);
278
- }
279
- stage.durationMs = durationMs;
280
- stagesByName.set(stage.name, stage);
281
- previousCompletedMs = stageCompletedMs;
282
- }
283
- if (!stagesByName.has(STAGE_NAMES[0])) throw new Error(`missing required stage ${STAGE_NAMES[0]}`);
284
- const resolutionStagePresent = stagesByName.has("resolve-provisional");
285
- if (task.resolutionAttempts > 0 && !resolutionStagePresent) {
286
- throw new Error("resolutionAttempts must agree with the resolve-provisional stage");
287
- }
288
- if (task.resolutionAttempts === 0 && resolutionStagePresent) {
289
- throw new Error("zero resolutionAttempts must omit the resolve-provisional stage");
290
- }
291
-
292
- if (!value.stages.every((stage, index) => stage.name === STAGE_NAMES[index])) {
293
- throw new Error("stages must use the documented workflow order");
294
- }
295
-
296
- const preparationOutcome = stagesByName.get("prepare-and-list-projects").outcome;
297
- const creationOutcome = stagesByName.get("create-thread")?.outcome;
298
- const recordingOutcome = stagesByName.get("record-task")?.outcome;
299
- const resolutionOutcome = stagesByName.get("resolve-provisional")?.outcome;
300
- if (preparationOutcome === "failed") {
301
- if (
302
- value.stages.length !== 1 || task.taskId !== null || task.threadId !== null || task.clientThreadId !== null ||
303
- task.recorded || task.resolution !== "unresolved" || task.resolutionAttempts !== 0
304
- ) throw new Error("failed preparation must stop the workflow unresolved");
305
- } else if (task.taskId === null || creationOutcome === undefined) {
306
- throw new Error("successful preparation requires a task ID and creation stage");
307
- }
308
- if (creationOutcome !== undefined && creationOutcome !== "failed" && recordingOutcome === undefined) {
309
- throw new Error("successful creation requires a record-task stage");
310
- }
311
- if (recordingOutcome !== undefined && (recordingOutcome === "recorded") !== task.recorded) {
312
- throw new Error("record-task outcome must agree with task.recorded");
313
- }
314
- if (creationOutcome === "durable") {
315
- if (task.resolution !== "immediate" || task.threadId === null || resolutionStagePresent) {
316
- throw new Error("durable creation must be an immediate durable resolution");
317
- }
318
- } else if (creationOutcome === "provisional") {
319
- if (task.clientThreadId === null) throw new Error("provisional creation requires a clientThreadId");
320
- if (task.recorded) {
321
- if (task.resolutionAttempts === 0) {
322
- if (resolutionStagePresent || task.resolution !== "unresolved" || task.threadId !== null) {
323
- throw new Error("zero-attempt provisional creation must remain unresolved");
324
- }
325
- } else if (
326
- !resolutionStagePresent ||
327
- (resolutionOutcome !== task.resolution && !(task.resolution === "unresolved" && resolutionOutcome === "failed"))
328
- ) {
329
- throw new Error("recorded provisional creation must agree with its resolution stage");
330
- }
331
- if ((task.resolution === "unresolved") !== (task.threadId === null)) {
332
- throw new Error("provisional resolution must agree with task.threadId");
333
- }
334
- } else if (
335
- resolutionStagePresent || task.resolution !== "unresolved" || task.threadId !== null || task.resolutionAttempts !== 0
336
- ) {
337
- throw new Error("unrecorded provisional creation must remain unresolved");
338
- }
339
- } else if (
340
- creationOutcome === "failed" && (
341
- value.stages.length !== 2 || task.resolution !== "unresolved" || task.threadId !== null ||
342
- task.clientThreadId !== null || task.recorded || resolutionStagePresent
343
- )
344
- ) {
345
- throw new Error("failed creation must remain unrecorded and unresolved");
346
- }
347
-
348
- const validation = requireObject(value.validation, "validation");
349
117
  const validationNames = [
350
- "recordVerified",
351
- "markerVerified",
352
- "outputVerified",
353
- "candidateFilterEffective",
118
+ "exactMarkerCorrelated", "noDispatcherPostCreateReads", "childIdentityVerified",
119
+ "parentIdentityRejected", "linkRetryVerified", "needsInputVerified",
120
+ "followUpTurnFresh", "dashboardDeepLinkVerified", "outputVerified",
354
121
  ];
355
- requireExactKeys(validation, validationNames, "validation");
122
+ exact(value.validation, validationNames, "validation");
356
123
  for (const name of validationNames) {
357
- if (typeof validation[name] !== "boolean") throw new Error(`validation.${name} must be boolean`);
358
- }
359
- if (validation.recordVerified && !task.recorded) throw new Error("recordVerified requires a recorded task");
360
- if (validation.markerVerified && task.threadId === null) throw new Error("markerVerified requires a durable threadId");
361
- if (validation.outputVerified && task.threadId === null) throw new Error("outputVerified requires a durable threadId");
362
-
363
- value.observations = normalizeObservations(
364
- value.observations,
365
- startedMs,
366
- completedMs,
367
- stagesByName.get("prepare-and-list-projects"),
368
- stagesByName.get("create-thread"),
369
- stagesByName.get("record-task"),
370
- stagesByName.get("resolve-provisional"),
371
- task.resolutionAttempts,
372
- );
373
- if (value.observations === undefined) delete value.observations;
374
- const snapshots = value.observations?.resolutionSnapshots ?? [];
375
- if (
376
- snapshots.length > 0 && snapshots.length !== task.resolutionAttempts &&
377
- !(resolutionOutcome === "failed" && snapshots.length === task.resolutionAttempts - 1)
378
- ) {
379
- throw new Error("resolutionSnapshots must account for every fallback attempt");
380
- }
381
- if (
382
- task.resolutionAttempts === 2 && snapshots.length !== 2 &&
383
- !(resolutionOutcome === "failed" && snapshots.length === 1)
384
- ) {
385
- throw new Error("two resolution attempts require two fallback snapshots");
386
- }
387
- if (task.resolution === "native" && (task.resolutionAttempts !== 1 || snapshots.length !== 0)) {
388
- throw new Error("native resolution requires exactly one non-snapshot attempt");
389
- }
390
- if (task.resolution === "discovered") {
391
- if (
392
- snapshots.length === 0 || snapshots.at(-1).exactMatchCount !== 1 ||
393
- snapshots.at(-1).resolveWriteOutcome !== "succeeded"
394
- ) {
395
- throw new Error("discovered resolution requires one successfully persisted final snapshot match");
396
- }
397
- }
398
- if (snapshots.some(
399
- (snapshot) => snapshot.attempt < task.resolutionAttempts && snapshot.exactMatchCount > 0,
400
- )) {
401
- throw new Error("any nonzero snapshot match count must terminate fallback resolution");
402
- }
403
- if (
404
- task.resolution === "unresolved" && snapshots.some(
405
- (snapshot) => snapshot.resolveWriteOutcome === "succeeded",
406
- )
407
- ) {
408
- throw new Error("unresolved resolution cannot have a successful resolve write");
409
- }
410
- if (
411
- snapshots.some((snapshot) => snapshot.resolveWriteOutcome === "failed") &&
412
- resolutionOutcome !== "failed"
413
- ) {
414
- throw new Error("a failed resolve write requires a failed resolution stage");
415
- }
416
- if (
417
- resolutionStagePresent && snapshots.length === 0 &&
418
- !["native", "unresolved"].includes(task.resolution)
419
- ) {
420
- throw new Error("non-snapshot resolution must be native or unresolved");
421
- }
422
- if (snapshots.length === 1 && snapshots[0].exactMatchCount === 0 && task.resolutionAttempts !== 2) {
423
- throw new Error("a completed first snapshot with zero matches requires the second fallback attempt");
424
- }
425
- const filterEffective = snapshots.length > 0 && snapshots.every(
426
- (snapshot) => snapshot.candidateCount < snapshot.recentTaskCount,
427
- );
428
- if (validation.candidateFilterEffective !== filterEffective) {
429
- throw new Error("candidateFilterEffective must match resolution snapshot counts");
124
+ if (typeof value.validation[name] !== "boolean") throw new Error(`validation.${name} must be boolean`);
125
+ if (!value.validation[name]) throw new Error(`validation.${name} must be true for a successful benchmark`);
430
126
  }
431
127
 
432
- const measuredStageMs = value.stages.reduce((total, stage) => total + stage.durationMs, 0);
433
- const totalWallMs = completedMs - startedMs;
434
128
  const summary = {
435
- totalWallMs,
436
- measuredStageMs,
437
- orchestrationOverheadMs: totalWallMs - measuredStageMs,
438
- resolved: task.threadId !== null,
439
- resolutionAttempts: task.resolutionAttempts,
129
+ totalWallMs: completed - started,
130
+ provisionalPath: value.task.clientThreadId !== null,
131
+ linked: value.task.linked,
132
+ freshFollowUp: value.turns.needsInput !== value.turns.completion,
440
133
  };
441
- if (requireDerived && value.summary === undefined) {
442
- throw new Error("summary is required in a saved result");
443
- }
134
+ if (requireDerived && value.summary === undefined) throw new Error("summary is required in a saved result");
444
135
  if (value.summary !== undefined) {
445
- const suppliedSummary = requireObject(value.summary, "summary");
446
- requireExactKeys(suppliedSummary, Object.keys(summary), "summary");
447
- if (Object.entries(summary).some(([name, expected]) => suppliedSummary[name] !== expected)) {
136
+ exact(value.summary, Object.keys(summary), "summary");
137
+ if (Object.entries(summary).some(([key, expected]) => value.summary[key] !== expected)) {
448
138
  throw new Error("summary does not match the derived benchmark summary");
449
139
  }
450
140
  }
@@ -460,8 +150,7 @@ export async function writeBenchmarkResult(input, outputDirectory) {
460
150
  const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
461
151
  const result = normalizeBenchmarkResult({ ...input, taskchefVersion: packageJson.version });
462
152
  await mkdir(outputDirectory, { recursive: true });
463
- const filename = `${timestampFilename(result.startedAt)}-${BENCHMARK_NAME}.json`;
464
- const outputPath = path.join(outputDirectory, filename);
153
+ const outputPath = path.join(outputDirectory, `${timestampFilename(result.startedAt)}-${BENCHMARK_NAME}.json`);
465
154
  await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, { flag: "wx" });
466
155
  return { outputPath, result };
467
156
  }
@@ -485,7 +174,7 @@ async function readJsonStdin() {
485
174
  let input = "";
486
175
  process.stdin.setEncoding("utf8");
487
176
  for await (const chunk of process.stdin) input += chunk;
488
- if (input.trim().length === 0) throw new Error("expected benchmark JSON on stdin");
177
+ if (input.trim() === "") throw new Error("expected benchmark JSON on stdin");
489
178
  return JSON.parse(input);
490
179
  }
491
180
 
@@ -19,9 +19,8 @@ all deterministic workspace operations.
19
19
  workspace.
20
20
  - Do not dispatch tasks or report on executor threads during bootstrap unless
21
21
  the user separately requests those actions.
22
- - Never create ad hoc hooks, schedules, polling, or daemons. The installed
23
- plugin's initial-identity hook is part of normal TaskChef execution, not
24
- bootstrap work.
22
+ - Never create hooks, schedules, polling, or daemons. TaskChef executors
23
+ self-link through the installed MCP server.
25
24
 
26
25
  ## Initialize and repair
27
26