taskchef 5.7.2 → 5.8.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.
- package/.codex-plugin/plugin.json +2 -2
- package/BACKLOG.md +4 -4
- package/README.md +78 -45
- package/SPEC.md +106 -89
- package/docs/delegation-design.md +158 -254
- package/hooks/hooks.json +18 -0
- package/hooks/taskchef-initial-prompt.js +17 -0
- package/index.js +6 -1
- package/package.json +2 -1
- package/skills/taskchef-bootstrap/SKILL.md +6 -3
- package/skills/taskchef-delegate/SKILL.md +49 -91
- package/skills/taskchef-report/SKILL.md +60 -27
- package/src/cli.js +8 -2
- package/src/delegation.js +109 -359
- package/src/hook.js +60 -0
- package/src/mcp.js +35 -2
- package/src/workspace-path.js +5 -1
- package/src/workspace.js +211 -14
package/src/delegation.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
// Deprecated v5 compatibility exports. TaskChef no longer uses bounded thread
|
|
4
|
+
// discovery, but keeping these pure helpers avoids breaking existing imports
|
|
5
|
+
// before the next major release.
|
|
3
6
|
export const THREAD_RESOLUTION_CHECKPOINTS_MS = Object.freeze([10_000, 30_000]);
|
|
4
7
|
export const THREAD_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
5
8
|
export const THREAD_RESOLUTION_RECENT_LIMIT = 50;
|
|
6
9
|
export const THREAD_RESOLUTION_CLOCK_SKEW_MS = 5_000;
|
|
7
10
|
export const EXECUTOR_OWNERSHIP_PARAGRAPH = "This task owns the delegated assignment. Execute it in this task; do not re-dispatch it merely because it concerns TaskChef or a configured project. Explicit requests to delegate separate work remain valid.";
|
|
11
|
+
export const EXECUTOR_RESULT_PARAGRAPH = "Before ending, call the TaskChef report_result MCP tool with completed, needs_input, or failed and a concise summary. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
|
|
8
12
|
|
|
9
13
|
const UUID_SOURCE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
10
14
|
const UUID_PATTERN = new RegExp(`^${UUID_SOURCE}$`);
|
|
@@ -52,6 +56,25 @@ function toolIdentifier(value) {
|
|
|
52
56
|
return normalized.length > 0 ? normalized : null;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
function attachCreationRecovery(error, taskId, resultReporting) {
|
|
60
|
+
const creationError = error instanceof Error ? error : new Error(String(error));
|
|
61
|
+
try {
|
|
62
|
+
Object.defineProperties(creationError, {
|
|
63
|
+
taskChefTaskId: { value: taskId, enumerable: true },
|
|
64
|
+
taskChefResultReporting: { value: resultReporting, enumerable: true },
|
|
65
|
+
});
|
|
66
|
+
return creationError;
|
|
67
|
+
} catch {
|
|
68
|
+
const wrapped = new Error(
|
|
69
|
+
`Executor creation failed for recorded TaskChef task ${taskId}.`,
|
|
70
|
+
{ cause: creationError },
|
|
71
|
+
);
|
|
72
|
+
wrapped.taskChefTaskId = taskId;
|
|
73
|
+
wrapped.taskChefResultReporting = resultReporting;
|
|
74
|
+
return wrapped;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
export function isProvisionalThreadId(value) {
|
|
56
79
|
const id = toolIdentifier(value);
|
|
57
80
|
return id !== null && id.startsWith("local:");
|
|
@@ -66,9 +89,6 @@ export function normalizeDurableThreadId(value, name = "threadId") {
|
|
|
66
89
|
return id;
|
|
67
90
|
}
|
|
68
91
|
|
|
69
|
-
function wait(delayMs) {
|
|
70
|
-
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
71
|
-
}
|
|
72
92
|
|
|
73
93
|
export function taskChefMarker(taskId) {
|
|
74
94
|
return `<!-- taskchef_id=${requireUuid(taskId)} -->`;
|
|
@@ -94,7 +114,7 @@ export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
|
94
114
|
const id = requireUuid(taskId);
|
|
95
115
|
return {
|
|
96
116
|
id,
|
|
97
|
-
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${instruction}`,
|
|
117
|
+
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${EXECUTOR_RESULT_PARAGRAPH}\n\n${instruction}`,
|
|
98
118
|
};
|
|
99
119
|
}
|
|
100
120
|
|
|
@@ -104,9 +124,7 @@ export function listThreadEntries(result) {
|
|
|
104
124
|
const unique = new Map();
|
|
105
125
|
for (const entry of entries) {
|
|
106
126
|
const id = toolIdentifier(entry?.id);
|
|
107
|
-
if (id !== null && !unique.has(id)) {
|
|
108
|
-
unique.set(id, { ...entry, id });
|
|
109
|
-
}
|
|
127
|
+
if (id !== null && !unique.has(id)) unique.set(id, { ...entry, id });
|
|
110
128
|
}
|
|
111
129
|
return [...unique.values()];
|
|
112
130
|
}
|
|
@@ -161,9 +179,9 @@ export function filterThreadCandidates(result, {
|
|
|
161
179
|
?? thread.environmentType
|
|
162
180
|
?? thread.backing?.environment?.type;
|
|
163
181
|
if (
|
|
164
|
-
environmentType !== null
|
|
165
|
-
candidateEnvironmentType !== undefined
|
|
166
|
-
candidateEnvironmentType !== environmentType
|
|
182
|
+
environmentType !== null
|
|
183
|
+
&& candidateEnvironmentType !== undefined
|
|
184
|
+
&& candidateEnvironmentType !== environmentType
|
|
167
185
|
) return false;
|
|
168
186
|
const candidateTime = timestampMilliseconds(thread.createdAt ?? thread.updatedAt);
|
|
169
187
|
if (minimumCreatedAt !== null && candidateTime !== null && candidateTime < minimumCreatedAt) {
|
|
@@ -176,173 +194,37 @@ export function filterThreadCandidates(result, {
|
|
|
176
194
|
Number(right.title === title) - Number(left.title === title));
|
|
177
195
|
}
|
|
178
196
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (!Number.isInteger(checkpoint) || checkpoint <= previous || checkpoint > timeoutMs) {
|
|
192
|
-
throw new Error("checkpointsMs must be strictly increasing positive integers within timeoutMs");
|
|
193
|
-
}
|
|
194
|
-
previous = checkpoint;
|
|
195
|
-
}
|
|
196
|
-
if (checkpointsMs.length === 2 && checkpointsMs[1] - checkpointsMs[0] < 20_000) {
|
|
197
|
-
throw new Error("checkpointsMs must keep two checkpoints at least 20000ms apart");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function inspectCandidates(candidates, {
|
|
202
|
-
readThread,
|
|
203
|
-
taskId,
|
|
204
|
-
attempt,
|
|
205
|
-
canVerify = () => true,
|
|
206
|
-
}) {
|
|
207
|
-
const inspected = await Promise.all(candidates.map(async (candidate) => {
|
|
208
|
-
try {
|
|
209
|
-
const readResult = await readThread({
|
|
210
|
-
threadId: candidate.id,
|
|
211
|
-
...(candidate.hostId ? { hostId: candidate.hostId } : {}),
|
|
212
|
-
turnLimit: 1,
|
|
213
|
-
includeOutputs: false,
|
|
214
|
-
});
|
|
215
|
-
if (!canVerify()) {
|
|
216
|
-
return { candidate, matches: false, error: null };
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
candidate,
|
|
220
|
-
matches: hasExactTaskChefMarker(readResult, taskId),
|
|
221
|
-
error: null,
|
|
222
|
-
};
|
|
223
|
-
} catch (error) {
|
|
224
|
-
return {
|
|
225
|
-
candidate,
|
|
226
|
-
matches: false,
|
|
227
|
-
error: {
|
|
228
|
-
attempt,
|
|
229
|
-
operation: "readThread",
|
|
230
|
-
threadId: candidate.id,
|
|
231
|
-
message: error instanceof Error ? error.message : String(error),
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
}));
|
|
236
|
-
return {
|
|
237
|
-
exactMatches: inspected.filter((item) => item.matches).map((item) => item.candidate),
|
|
238
|
-
errors: inspected.flatMap((item) => item.error === null ? [] : [item.error]),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export async function createAndRecordDelegation({
|
|
243
|
-
project,
|
|
244
|
-
title,
|
|
245
|
-
instruction,
|
|
246
|
-
target,
|
|
247
|
-
expected = {},
|
|
248
|
-
createThread,
|
|
249
|
-
listThreads,
|
|
250
|
-
readThread,
|
|
251
|
-
recordTask,
|
|
252
|
-
resolveRecordedTask = null,
|
|
253
|
-
resolveProvisionalThread = null,
|
|
254
|
-
taskId = randomUUID(),
|
|
255
|
-
checkpointsMs = THREAD_RESOLUTION_CHECKPOINTS_MS,
|
|
256
|
-
timeoutMs = THREAD_RESOLUTION_TIMEOUT_MS,
|
|
257
|
-
recentLimit = THREAD_RESOLUTION_RECENT_LIMIT,
|
|
258
|
-
now = Date.now,
|
|
259
|
-
waitImpl = wait,
|
|
260
|
-
}) {
|
|
197
|
+
export async function createAndRecordDelegation(input) {
|
|
198
|
+
const {
|
|
199
|
+
project,
|
|
200
|
+
title,
|
|
201
|
+
instruction,
|
|
202
|
+
target,
|
|
203
|
+
createThread,
|
|
204
|
+
recordTask,
|
|
205
|
+
resolveRecordedTask = null,
|
|
206
|
+
reportRecordedResult = null,
|
|
207
|
+
taskId = randomUUID(),
|
|
208
|
+
} = input ?? {};
|
|
261
209
|
requireString(project, "project");
|
|
262
210
|
requireString(title, "title");
|
|
263
211
|
requireObject(target, "target");
|
|
264
212
|
for (const [value, name] of [
|
|
265
213
|
[createThread, "createThread"],
|
|
266
|
-
[listThreads, "listThreads"],
|
|
267
|
-
[readThread, "readThread"],
|
|
268
214
|
[recordTask, "recordTask"],
|
|
269
215
|
]) {
|
|
270
216
|
if (typeof value !== "function") throw new Error(`${name} must be a function`);
|
|
271
217
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
218
|
+
for (const [value, name] of [
|
|
219
|
+
[resolveRecordedTask, "resolveRecordedTask"],
|
|
220
|
+
[reportRecordedResult, "reportRecordedResult"],
|
|
221
|
+
]) {
|
|
222
|
+
if (value !== null && typeof value !== "function") {
|
|
223
|
+
throw new Error(`${name} must be a function or null`);
|
|
224
|
+
}
|
|
277
225
|
}
|
|
278
|
-
validateResolutionSchedule(checkpointsMs, timeoutMs);
|
|
279
226
|
|
|
280
227
|
const prepared = prepareDelegation(instruction, { taskId });
|
|
281
|
-
const discoveryErrors = [];
|
|
282
|
-
const createdAfter = now();
|
|
283
|
-
const createResult = parseToolResult(await createThread({
|
|
284
|
-
prompt: prepared.instruction,
|
|
285
|
-
title,
|
|
286
|
-
target,
|
|
287
|
-
}), "create_thread result");
|
|
288
|
-
const clientThreadId = toolIdentifier(createResult.clientThreadId);
|
|
289
|
-
const pendingWorktreeId = toolIdentifier(createResult.pendingWorktreeId);
|
|
290
|
-
const returnedThreadId = toolIdentifier(createResult.threadId);
|
|
291
|
-
const returnedProvisionalId = isProvisionalThreadId(returnedThreadId)
|
|
292
|
-
? returnedThreadId
|
|
293
|
-
: null;
|
|
294
|
-
const provisional = clientThreadId ?? pendingWorktreeId ?? returnedProvisionalId;
|
|
295
|
-
const resolutionStartedAt = provisional === null ? null : now();
|
|
296
|
-
const provisionalIds = new Set([
|
|
297
|
-
clientThreadId,
|
|
298
|
-
pendingWorktreeId,
|
|
299
|
-
returnedProvisionalId,
|
|
300
|
-
].filter(Boolean));
|
|
301
|
-
const durableThreadId = !isProvisionalThreadId(returnedThreadId)
|
|
302
|
-
&& !provisionalIds.has(returnedThreadId)
|
|
303
|
-
? returnedThreadId
|
|
304
|
-
: null;
|
|
305
|
-
|
|
306
|
-
if (durableThreadId !== null) {
|
|
307
|
-
await recordTask({
|
|
308
|
-
id: prepared.id,
|
|
309
|
-
project,
|
|
310
|
-
title,
|
|
311
|
-
instruction: prepared.instruction,
|
|
312
|
-
threadId: durableThreadId,
|
|
313
|
-
});
|
|
314
|
-
return {
|
|
315
|
-
status: "recorded",
|
|
316
|
-
resolution: "immediate",
|
|
317
|
-
...prepared,
|
|
318
|
-
threadId: durableThreadId,
|
|
319
|
-
hostId: createResult.hostId ?? expected.hostId ?? null,
|
|
320
|
-
provisional,
|
|
321
|
-
attempts: 0,
|
|
322
|
-
discoveryErrors,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (provisional === null) {
|
|
327
|
-
await recordTask({
|
|
328
|
-
id: prepared.id,
|
|
329
|
-
project,
|
|
330
|
-
title,
|
|
331
|
-
instruction: prepared.instruction,
|
|
332
|
-
threadId: null,
|
|
333
|
-
});
|
|
334
|
-
return {
|
|
335
|
-
status: "recorded-unresolved",
|
|
336
|
-
reason: "create-returned-no-thread-identifier",
|
|
337
|
-
...prepared,
|
|
338
|
-
threadId: null,
|
|
339
|
-
provisional: null,
|
|
340
|
-
attempts: 0,
|
|
341
|
-
matchingThreadIds: [],
|
|
342
|
-
discoveryErrors,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
228
|
await recordTask({
|
|
347
229
|
id: prepared.id,
|
|
348
230
|
project,
|
|
@@ -351,219 +233,87 @@ export async function createAndRecordDelegation({
|
|
|
351
233
|
threadId: null,
|
|
352
234
|
});
|
|
353
235
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
236
|
+
let createResult;
|
|
237
|
+
try {
|
|
238
|
+
createResult = parseToolResult(await createThread({
|
|
239
|
+
prompt: prepared.instruction,
|
|
240
|
+
title,
|
|
241
|
+
target,
|
|
242
|
+
}), "create_thread result");
|
|
243
|
+
} catch (error) {
|
|
244
|
+
let resultReporting = "unavailable";
|
|
245
|
+
if (reportRecordedResult !== null) {
|
|
246
|
+
try {
|
|
247
|
+
await reportRecordedResult({
|
|
248
|
+
taskId: prepared.id,
|
|
249
|
+
threadId: null,
|
|
250
|
+
turnId: null,
|
|
251
|
+
status: "failed",
|
|
252
|
+
summary: "Executor creation failed before the executor started.",
|
|
253
|
+
});
|
|
254
|
+
resultReporting = "recorded";
|
|
255
|
+
} catch {
|
|
256
|
+
resultReporting = "failed";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw attachCreationRecovery(error, prepared.id, resultReporting);
|
|
365
260
|
}
|
|
366
261
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const canContinue = enforceDeadline ? hasResolutionTime : () => true;
|
|
380
|
-
const threadId = toolIdentifier(thread.id);
|
|
381
|
-
if (
|
|
382
|
-
threadId === null
|
|
383
|
-
|| provisionalIds.has(threadId)
|
|
384
|
-
|| isProvisionalThreadId(threadId)
|
|
385
|
-
) {
|
|
386
|
-
discoveryErrors.push({
|
|
387
|
-
attempt,
|
|
388
|
-
operation: "validateThreadId",
|
|
389
|
-
...(threadId === null ? {} : { threadId }),
|
|
390
|
-
message: "resolved threadId is not a durable identifier",
|
|
391
|
-
});
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
const durableThread = { ...thread, id: threadId };
|
|
395
|
-
if (!canContinue()) return null;
|
|
396
|
-
if (!verified) {
|
|
397
|
-
const inspected = await inspectCandidates([durableThread], {
|
|
398
|
-
readThread,
|
|
399
|
-
taskId: prepared.id,
|
|
400
|
-
attempt,
|
|
401
|
-
canVerify: canContinue,
|
|
402
|
-
});
|
|
403
|
-
discoveryErrors.push(...inspected.errors);
|
|
404
|
-
if (inspected.exactMatches.length !== 1 || inspected.errors.length > 0) return null;
|
|
405
|
-
}
|
|
406
|
-
if (!canContinue()) return null;
|
|
407
|
-
try {
|
|
408
|
-
await resolveRecordedTask({ id: prepared.id, threadId });
|
|
409
|
-
} catch (error) {
|
|
410
|
-
discoveryErrors.push({
|
|
411
|
-
attempt,
|
|
412
|
-
operation: "resolveRecordedTask",
|
|
413
|
-
threadId,
|
|
414
|
-
message: error instanceof Error ? error.message : String(error),
|
|
415
|
-
});
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
return {
|
|
419
|
-
status: "recorded",
|
|
420
|
-
resolution,
|
|
421
|
-
...prepared,
|
|
422
|
-
threadId,
|
|
423
|
-
hostId: durableThread.hostId ?? expected.hostId ?? null,
|
|
424
|
-
provisional,
|
|
425
|
-
attempts: attempt,
|
|
426
|
-
discoveryErrors,
|
|
427
|
-
};
|
|
428
|
-
};
|
|
262
|
+
const returnedThreadId = toolIdentifier(createResult.threadId);
|
|
263
|
+
const clientThreadId = toolIdentifier(createResult.clientThreadId);
|
|
264
|
+
const pendingWorktreeId = toolIdentifier(createResult.pendingWorktreeId);
|
|
265
|
+
const provisional = clientThreadId
|
|
266
|
+
?? pendingWorktreeId
|
|
267
|
+
?? (isProvisionalThreadId(returnedThreadId) ? returnedThreadId : null);
|
|
268
|
+
const durableThreadId = returnedThreadId !== null
|
|
269
|
+
&& !isProvisionalThreadId(returnedThreadId)
|
|
270
|
+
&& returnedThreadId !== clientThreadId
|
|
271
|
+
&& returnedThreadId !== pendingWorktreeId
|
|
272
|
+
? returnedThreadId
|
|
273
|
+
: null;
|
|
429
274
|
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
let nativeAttempts = 0;
|
|
433
|
-
const remainingTimeoutMs = timeoutMs - (now() - resolutionStartedAt);
|
|
434
|
-
if (remainingTimeoutMs > 0) {
|
|
435
|
-
nativeAttempts = 1;
|
|
275
|
+
if (durableThreadId !== null) {
|
|
276
|
+
if (resolveRecordedTask !== null) {
|
|
436
277
|
try {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
278
|
+
await resolveRecordedTask({ id: prepared.id, threadId: durableThreadId });
|
|
279
|
+
return {
|
|
280
|
+
status: "recorded",
|
|
281
|
+
resolution: "immediate",
|
|
282
|
+
...prepared,
|
|
283
|
+
threadId: durableThreadId,
|
|
284
|
+
provisional,
|
|
285
|
+
hostId: createResult.hostId ?? null,
|
|
286
|
+
};
|
|
446
287
|
} catch (error) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
288
|
+
return {
|
|
289
|
+
status: "recorded-unresolved",
|
|
290
|
+
reason: "task-resolution-failed",
|
|
291
|
+
resolutionError: error instanceof Error ? error.message : String(error),
|
|
292
|
+
...prepared,
|
|
293
|
+
threadId: null,
|
|
294
|
+
createdThreadId: durableThreadId,
|
|
295
|
+
provisional,
|
|
296
|
+
hostId: createResult.hostId ?? null,
|
|
297
|
+
};
|
|
454
298
|
}
|
|
455
299
|
}
|
|
456
|
-
hasResolutionTime();
|
|
457
|
-
const nativeThreadId = toolIdentifier(nativeResult?.threadId);
|
|
458
|
-
if (nativeThreadId !== null) {
|
|
459
|
-
const resolved = await acceptResolvedThread({
|
|
460
|
-
id: nativeThreadId,
|
|
461
|
-
hostId: nativeResult.hostId ?? expected.hostId ?? null,
|
|
462
|
-
}, "native", 1);
|
|
463
|
-
if (resolved !== null) return resolved;
|
|
464
|
-
}
|
|
465
300
|
return {
|
|
466
301
|
status: "recorded-unresolved",
|
|
467
|
-
reason:
|
|
468
|
-
? "thread-discovery-error"
|
|
469
|
-
: deadlineExpired
|
|
470
|
-
? "resolution-deadline-exhausted"
|
|
471
|
-
: "native-resolution-unresolved",
|
|
302
|
+
reason: "task-resolution-unavailable",
|
|
472
303
|
...prepared,
|
|
473
304
|
threadId: null,
|
|
305
|
+
createdThreadId: durableThreadId,
|
|
474
306
|
provisional,
|
|
475
|
-
|
|
476
|
-
matchingThreadIds: [],
|
|
477
|
-
discoveryErrors,
|
|
307
|
+
hostId: createResult.hostId ?? null,
|
|
478
308
|
};
|
|
479
309
|
}
|
|
480
310
|
|
|
481
|
-
let exactMatches = [];
|
|
482
|
-
let ambiguousMatches = [];
|
|
483
|
-
let attemptsMade = 0;
|
|
484
|
-
let previousAttemptStartedAt = resolutionStartedAt;
|
|
485
|
-
for (let index = 0; index < checkpointsMs.length; index += 1) {
|
|
486
|
-
const attempt = index + 1;
|
|
487
|
-
const intervalMs = index === 0
|
|
488
|
-
? checkpointsMs[0]
|
|
489
|
-
: checkpointsMs[index] - checkpointsMs[index - 1];
|
|
490
|
-
const dueAt = previousAttemptStartedAt + intervalMs;
|
|
491
|
-
const remainingDelayMs = dueAt - now();
|
|
492
|
-
if (remainingDelayMs > 0) {
|
|
493
|
-
try {
|
|
494
|
-
await waitImpl(remainingDelayMs);
|
|
495
|
-
} catch (error) {
|
|
496
|
-
discoveryErrors.push({
|
|
497
|
-
attempt,
|
|
498
|
-
operation: "wait",
|
|
499
|
-
message: error instanceof Error ? error.message : String(error),
|
|
500
|
-
});
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
previousAttemptStartedAt = now();
|
|
505
|
-
attemptsMade = attempt;
|
|
506
|
-
let candidates = [];
|
|
507
|
-
let attemptFailed = false;
|
|
508
|
-
try {
|
|
509
|
-
const snapshot = await listThreads({ limit: recentLimit });
|
|
510
|
-
candidates = filterThreadCandidates(snapshot, {
|
|
511
|
-
excludedThreadIds: provisionalIds,
|
|
512
|
-
hostId: expected.hostId ?? null,
|
|
513
|
-
projectId: expected.projectId ?? target.projectId ?? null,
|
|
514
|
-
title,
|
|
515
|
-
createdAfter,
|
|
516
|
-
environmentType: expected.environmentType ?? target.environment?.type ?? null,
|
|
517
|
-
});
|
|
518
|
-
} catch (error) {
|
|
519
|
-
attemptFailed = true;
|
|
520
|
-
discoveryErrors.push({
|
|
521
|
-
attempt,
|
|
522
|
-
operation: "listThreads",
|
|
523
|
-
message: error instanceof Error ? error.message : String(error),
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
const inspected = await inspectCandidates(candidates, {
|
|
527
|
-
readThread,
|
|
528
|
-
taskId: prepared.id,
|
|
529
|
-
attempt,
|
|
530
|
-
});
|
|
531
|
-
exactMatches = inspected.exactMatches;
|
|
532
|
-
discoveryErrors.push(...inspected.errors);
|
|
533
|
-
if (inspected.errors.length > 0) attemptFailed = true;
|
|
534
|
-
if (exactMatches.length > 1) ambiguousMatches = exactMatches;
|
|
535
|
-
if (
|
|
536
|
-
exactMatches.length === 1
|
|
537
|
-
&& ambiguousMatches.length === 0
|
|
538
|
-
&& !attemptFailed
|
|
539
|
-
&& discoveryErrors.length === 0
|
|
540
|
-
) {
|
|
541
|
-
const resolved = await acceptResolvedThread(
|
|
542
|
-
exactMatches[0],
|
|
543
|
-
"discovered",
|
|
544
|
-
attempt,
|
|
545
|
-
{ verified: true, enforceDeadline: false },
|
|
546
|
-
);
|
|
547
|
-
if (resolved !== null) return resolved;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const reason = ambiguousMatches.length > 1
|
|
552
|
-
? "multiple-exact-marker-matches"
|
|
553
|
-
: discoveryErrors.length > 0
|
|
554
|
-
? "thread-discovery-error"
|
|
555
|
-
: deadlineExpired
|
|
556
|
-
? "resolution-deadline-exhausted"
|
|
557
|
-
: "no-exact-marker-match";
|
|
558
311
|
return {
|
|
559
312
|
status: "recorded-unresolved",
|
|
560
|
-
reason,
|
|
313
|
+
reason: "awaiting-initial-hook",
|
|
561
314
|
...prepared,
|
|
562
315
|
threadId: null,
|
|
563
316
|
provisional,
|
|
564
|
-
|
|
565
|
-
discoveryErrors,
|
|
566
|
-
matchingThreadIds: (ambiguousMatches.length > 1 ? ambiguousMatches : exactMatches)
|
|
567
|
-
.map((thread) => thread.id),
|
|
317
|
+
hostId: createResult.hostId ?? null,
|
|
568
318
|
};
|
|
569
319
|
}
|
package/src/hook.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parseTaskChefMarker } from "./delegation.js";
|
|
2
|
+
import { listTasks, startTaskFromHook } from "./workspace.js";
|
|
3
|
+
import { resolveWorkspacePath } from "./workspace-path.js";
|
|
4
|
+
|
|
5
|
+
function nonEmptyString(value) {
|
|
6
|
+
return typeof value === "string" && value.trim().length > 0
|
|
7
|
+
? value.trim()
|
|
8
|
+
: null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hookContext(taskId, threadId, turnId) {
|
|
12
|
+
return {
|
|
13
|
+
continue: true,
|
|
14
|
+
hookSpecificOutput: {
|
|
15
|
+
hookEventName: "UserPromptSubmit",
|
|
16
|
+
additionalContext: [
|
|
17
|
+
`This is TaskChef task ${taskId} in root thread ${threadId}, current turn ${turnId}.`,
|
|
18
|
+
"Before ending the task, call the TaskChef report_result MCP tool with the semantic outcome needs_input, completed, or failed and a concise summary.",
|
|
19
|
+
"Pass this exact task ID, root thread ID, and current turn ID to report_result.",
|
|
20
|
+
"Use needs_input only for a decision or information the user must provide; native approval prompts are reported from live Codex state.",
|
|
21
|
+
"Do not include secrets, transcripts, or raw command output in the summary.",
|
|
22
|
+
].join(" "),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleInitialPromptHook(input, {
|
|
28
|
+
workspace = null,
|
|
29
|
+
resolveWorkspace = () => resolveWorkspacePath().workspace,
|
|
30
|
+
startTask = startTaskFromHook,
|
|
31
|
+
listTaskSnapshots = listTasks,
|
|
32
|
+
} = {}) {
|
|
33
|
+
if (input?.hook_event_name !== "UserPromptSubmit") return { continue: true };
|
|
34
|
+
const taskId = parseTaskChefMarker(input.prompt);
|
|
35
|
+
const threadId = nonEmptyString(input.session_id);
|
|
36
|
+
const turnId = nonEmptyString(input.turn_id);
|
|
37
|
+
if (threadId === null || turnId === null) {
|
|
38
|
+
if (taskId === null) return { continue: true };
|
|
39
|
+
return {
|
|
40
|
+
continue: true,
|
|
41
|
+
systemMessage: "TaskChef could not link this initial task because the hook payload lacked a session or turn ID.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (taskId !== null) {
|
|
45
|
+
const root = workspace ?? resolveWorkspace();
|
|
46
|
+
await startTask(root, taskId, threadId, turnId);
|
|
47
|
+
return hookContext(taskId, threadId, turnId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const root = workspace ?? resolveWorkspace();
|
|
52
|
+
const task = (await listTaskSnapshots(root)).find((item) => item.threadId === threadId);
|
|
53
|
+
return task === undefined
|
|
54
|
+
? { continue: true }
|
|
55
|
+
: hookContext(task.id, threadId, turnId);
|
|
56
|
+
} catch {
|
|
57
|
+
// An unrelated prompt must not surface TaskChef workspace setup failures.
|
|
58
|
+
return { continue: true };
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import {
|
|
4
4
|
prepareDispatch,
|
|
5
5
|
recordTask,
|
|
6
|
+
reportTaskResult,
|
|
6
7
|
resolveTask,
|
|
7
8
|
} from "./workspace.js";
|
|
8
9
|
import { parseTaskChefMarker } from "./delegation.js";
|
|
@@ -24,6 +25,11 @@ const taskSchema = z.object({
|
|
|
24
25
|
instruction: z.string(),
|
|
25
26
|
threadId: z.string().nullable(),
|
|
26
27
|
createdAt: z.string(),
|
|
28
|
+
status: z.enum(["working", "needs_input", "completed", "failed"]).nullable(),
|
|
29
|
+
summary: z.string().nullable(),
|
|
30
|
+
turnId: z.string().nullable(),
|
|
31
|
+
updatedAt: z.string().nullable(),
|
|
32
|
+
updatedBy: z.enum(["dispatcher", "hook", "mcp"]).nullable(),
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
const preparationSchema = z.object({
|
|
@@ -47,13 +53,14 @@ export function createTaskChefMcpServer({
|
|
|
47
53
|
workspace = resolveWorkspacePath().workspace,
|
|
48
54
|
prepare = prepareDispatch,
|
|
49
55
|
record = recordTask,
|
|
56
|
+
reportResult = reportTaskResult,
|
|
50
57
|
resolve = resolveTask,
|
|
51
58
|
} = {}) {
|
|
52
59
|
const server = new McpServer(
|
|
53
60
|
{ name: "taskchef", version: "1.0.0" },
|
|
54
61
|
{
|
|
55
62
|
instructions:
|
|
56
|
-
"Prepare with prepare_dispatch,
|
|
63
|
+
"Prepare with prepare_dispatch, call record_task before creating the Codex task, then create it natively. Use resolve_task for a durable root thread ID. Executors must call report_result before ending with completed, needs_input, or failed.",
|
|
57
64
|
},
|
|
58
65
|
);
|
|
59
66
|
|
|
@@ -82,7 +89,7 @@ export function createTaskChefMcpServer({
|
|
|
82
89
|
{
|
|
83
90
|
title: "Record TaskChef task",
|
|
84
91
|
description:
|
|
85
|
-
"Atomically append one
|
|
92
|
+
"Atomically append one prepared TaskChef task before creating its Codex executor. Pass the exact marked instruction and use null for threadId until a durable root ID is known.",
|
|
86
93
|
inputSchema: {
|
|
87
94
|
id: z.string().min(1),
|
|
88
95
|
project: z.string().min(1),
|
|
@@ -129,5 +136,31 @@ export function createTaskChefMcpServer({
|
|
|
129
136
|
},
|
|
130
137
|
);
|
|
131
138
|
|
|
139
|
+
server.registerTool(
|
|
140
|
+
"report_result",
|
|
141
|
+
{
|
|
142
|
+
title: "Report TaskChef result",
|
|
143
|
+
description:
|
|
144
|
+
"Store the executor's latest semantic outcome for one recorded TaskChef task. A linked executor must supply its matching durable thread ID and current turn ID. Null IDs are accepted only for a failed executor creation before a thread exists. Use needs_input only for a semantic user decision, not a transient native approval prompt. Summaries must omit secrets, transcripts, and raw command output.",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
taskId: z.string().min(1),
|
|
147
|
+
threadId: z.string().min(1).nullable(),
|
|
148
|
+
turnId: z.string().min(1).max(256).nullable(),
|
|
149
|
+
status: z.enum(["needs_input", "completed", "failed"]),
|
|
150
|
+
summary: z.string().min(1).max(2_000),
|
|
151
|
+
},
|
|
152
|
+
outputSchema: { task: taskSchema },
|
|
153
|
+
annotations: {
|
|
154
|
+
readOnlyHint: false,
|
|
155
|
+
destructiveHint: true,
|
|
156
|
+
openWorldHint: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
async (input) => {
|
|
160
|
+
const task = await reportResult(workspace, input);
|
|
161
|
+
return toolResult("task", task, `Recorded ${task.status} result for TaskChef task ${task.id}.`);
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
132
165
|
return server;
|
|
133
166
|
}
|