taskchef 5.7.2 → 5.9.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/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 validateResolutionSchedule(checkpointsMs, timeoutMs) {
180
- if (!Array.isArray(checkpointsMs) || checkpointsMs.length === 0) {
181
- throw new Error("checkpointsMs must contain at least one checkpoint");
182
- }
183
- if (checkpointsMs.length > 2) {
184
- throw new Error("checkpointsMs must contain at most two checkpoints");
185
- }
186
- if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
187
- throw new Error("timeoutMs must be positive");
188
- }
189
- let previous = 0;
190
- for (const checkpoint of checkpointsMs) {
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
- if (resolveProvisionalThread !== null && typeof resolveProvisionalThread !== "function") {
273
- throw new Error("resolveProvisionalThread must be a function or null");
274
- }
275
- if (resolveRecordedTask !== null && typeof resolveRecordedTask !== "function") {
276
- throw new Error("resolveRecordedTask must be a function or null");
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
- if (resolveRecordedTask === null) {
355
- return {
356
- status: "recorded-unresolved",
357
- reason: "task-resolution-unavailable",
358
- ...prepared,
359
- threadId: null,
360
- provisional,
361
- attempts: 0,
362
- matchingThreadIds: [],
363
- discoveryErrors,
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 resolutionDeadline = resolutionStartedAt + timeoutMs;
368
- let deadlineExpired = false;
369
- const hasResolutionTime = () => {
370
- if (now() < resolutionDeadline) return true;
371
- deadlineExpired = true;
372
- return false;
373
- };
374
-
375
- const acceptResolvedThread = async (thread, resolution, attempt, {
376
- verified = false,
377
- enforceDeadline = true,
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 (resolveProvisionalThread !== null) {
431
- let nativeResult = null;
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
- const nativeResponse = await resolveProvisionalThread({
438
- provisionalId: provisional,
439
- ...(clientThreadId ? { clientThreadId } : {}),
440
- ...(pendingWorktreeId ? { pendingWorktreeId } : {}),
441
- timeoutMs: remainingTimeoutMs,
442
- });
443
- if (hasResolutionTime()) {
444
- nativeResult = parseToolResult(nativeResponse, "provisional thread resolver result");
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
- if (hasResolutionTime()) {
448
- discoveryErrors.push({
449
- attempt: 1,
450
- operation: "resolveProvisionalThread",
451
- message: error instanceof Error ? error.message : String(error),
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: discoveryErrors.length > 0
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
- attempts: nativeAttempts,
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
- attempts: attemptsMade,
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, create the Codex task natively, then call record_task exactly once. Use resolve_task only after one exact structured marker match.",
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 created Codex task to the canonical TaskChef history. Pass the exact marked instruction returned by preparation and never pass a provisional ID as threadId.",
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
  }