taskchef 5.11.2 → 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.
- package/.codex-plugin/plugin.json +1 -1
- package/BACKLOG.md +6 -15
- package/README.md +30 -40
- package/SPEC.md +92 -369
- package/assets/e2e-benchmark-example.json +37 -33
- package/docs/delegation-design.md +75 -176
- package/index.js +3 -11
- package/package.json +1 -2
- package/scripts/e2e-benchmark.js +101 -412
- package/skills/taskchef-bootstrap/SKILL.md +2 -3
- package/skills/taskchef-delegate/SKILL.md +60 -138
- package/skills/taskchef-report/SKILL.md +12 -8
- package/src/cli.js +2 -1
- package/src/dashboard/actions.js +17 -0
- package/src/dashboard/app.js +15 -12
- package/src/dashboard/index.html +1 -1
- package/src/dashboard/styles.css +4 -0
- package/src/dashboard.js +1 -0
- package/src/delegation.js +24 -201
- package/src/mcp.js +10 -10
- package/src/workspace.js +128 -61
- package/hooks/hooks.json +0 -18
- package/hooks/taskchef-initial-prompt.js +0 -17
- package/src/hook.js +0 -96
package/src/delegation.js
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
// Bounded discovery parameters used when Codex returns only a provisional
|
|
4
|
-
// client ID. Candidate content still needs exact-marker verification.
|
|
5
|
-
export const THREAD_RESOLUTION_CHECKPOINTS_MS = Object.freeze([10_000, 30_000]);
|
|
6
|
-
export const THREAD_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
7
|
-
export const THREAD_RESOLUTION_RECENT_LIMIT = 50;
|
|
8
|
-
export const THREAD_RESOLUTION_CLOCK_SKEW_MS = 5_000;
|
|
9
3
|
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.";
|
|
10
|
-
export const
|
|
4
|
+
export const EXECUTOR_LINK_PARAGRAPH = "Before any other work, read this executor's own durable Codex thread ID from the current task's CODEX_THREAD_ID environment value and call the TaskChef link_task MCP tool with that thread ID and the marked TaskChef task ID. Never use CODEX_SESSION_ID or the parent or delegator thread ID. If linking fails, CODEX_THREAD_ID is unavailable, or the tool is unavailable, report the failure visibly and retry on a later turn; do not guess an identity or continue substantive work while the task is link-pending.";
|
|
5
|
+
export const EXECUTOR_RESULT_PARAGRAPH = "Before ending, call the TaskChef report_result MCP tool with the marked task ID, this executor's self-linked thread ID, the current turn ID from an exact native read of that same thread, completed, needs_input, or failed, and a concise summary. Never reuse a prior turn ID after a follow-up. 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.";
|
|
11
6
|
|
|
12
7
|
const UUID_SOURCE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
13
8
|
const UUID_PATTERN = new RegExp(`^${UUID_SOURCE}$`);
|
|
9
|
+
const CODEX_UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
14
10
|
const TASKCHEF_MARKER_PATTERN = new RegExp(`^<!-- taskchef_id=(${UUID_SOURCE}) -->$`);
|
|
15
11
|
const LEGACY_TASKCHEF_MARKER_PATTERN = new RegExp(`^# taskchef_id=(${UUID_SOURCE})$`);
|
|
16
12
|
|
|
17
13
|
function requireObject(value, name) {
|
|
18
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
19
|
-
throw new Error(`${name} must be an object`);
|
|
20
|
-
}
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
|
|
21
15
|
return value;
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
function requireString(value, name) {
|
|
25
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
26
|
-
throw new Error(`${name} must be a non-empty string`);
|
|
27
|
-
}
|
|
19
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${name} must be a non-empty string`);
|
|
28
20
|
return value;
|
|
29
21
|
}
|
|
30
22
|
|
|
@@ -44,11 +36,6 @@ function parseToolResult(value, name) {
|
|
|
44
36
|
}
|
|
45
37
|
}
|
|
46
38
|
|
|
47
|
-
function timestampMilliseconds(value) {
|
|
48
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
49
|
-
return value < 1_000_000_000_000 ? value * 1_000 : value;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
39
|
function toolIdentifier(value) {
|
|
53
40
|
if (typeof value !== "string") return null;
|
|
54
41
|
const normalized = value.trim();
|
|
@@ -64,10 +51,7 @@ function attachCreationRecovery(error, taskId, resultReporting) {
|
|
|
64
51
|
});
|
|
65
52
|
return creationError;
|
|
66
53
|
} catch {
|
|
67
|
-
const wrapped = new Error(
|
|
68
|
-
`Executor creation failed for recorded TaskChef task ${taskId}.`,
|
|
69
|
-
{ cause: creationError },
|
|
70
|
-
);
|
|
54
|
+
const wrapped = new Error(`Executor creation failed for recorded TaskChef task ${taskId}.`, { cause: creationError });
|
|
71
55
|
wrapped.taskChefTaskId = taskId;
|
|
72
56
|
wrapped.taskChefResultReporting = resultReporting;
|
|
73
57
|
return wrapped;
|
|
@@ -82,12 +66,17 @@ export function isProvisionalThreadId(value) {
|
|
|
82
66
|
export function normalizeDurableThreadId(value, name = "threadId") {
|
|
83
67
|
const id = toolIdentifier(value);
|
|
84
68
|
if (id === null) throw new Error(`${name} must be a non-empty string`);
|
|
85
|
-
if (isProvisionalThreadId(id)) {
|
|
86
|
-
throw new Error(`${name} must be a durable thread ID, not a provisional local ID`);
|
|
87
|
-
}
|
|
69
|
+
if (isProvisionalThreadId(id)) throw new Error(`${name} must be a durable thread ID, not a provisional local ID`);
|
|
88
70
|
return id;
|
|
89
71
|
}
|
|
90
72
|
|
|
73
|
+
export function normalizeCodexThreadId(value, name = "threadId") {
|
|
74
|
+
const id = normalizeDurableThreadId(value, name).toLowerCase();
|
|
75
|
+
if (!CODEX_UUID_V7_PATTERN.test(id)) {
|
|
76
|
+
throw new Error(`${name} must be a canonical Codex UUIDv7`);
|
|
77
|
+
}
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
91
80
|
|
|
92
81
|
export function taskChefMarker(taskId) {
|
|
93
82
|
return `<!-- taskchef_id=${requireUuid(taskId)} -->`;
|
|
@@ -107,149 +96,35 @@ export function parseTaskChefMarker(instruction, { allowLegacyHeading = false }
|
|
|
107
96
|
|
|
108
97
|
export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
109
98
|
requireString(instruction, "instruction");
|
|
110
|
-
if (parseTaskChefMarker(instruction) !== null)
|
|
111
|
-
throw new Error("instruction already contains a TaskChef marker");
|
|
112
|
-
}
|
|
99
|
+
if (parseTaskChefMarker(instruction) !== null) throw new Error("instruction already contains a TaskChef marker");
|
|
113
100
|
const id = requireUuid(taskId);
|
|
114
101
|
return {
|
|
115
102
|
id,
|
|
116
|
-
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${EXECUTOR_RESULT_PARAGRAPH}\n\n${instruction}`,
|
|
103
|
+
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${EXECUTOR_LINK_PARAGRAPH}\n\n${EXECUTOR_RESULT_PARAGRAPH}\n\n${instruction}`,
|
|
117
104
|
};
|
|
118
105
|
}
|
|
119
106
|
|
|
120
|
-
export function listThreadEntries(result) {
|
|
121
|
-
const parsed = parseToolResult(result, "list_threads result");
|
|
122
|
-
const entries = [...(parsed.pinnedThreads ?? []), ...(parsed.threads ?? [])];
|
|
123
|
-
const unique = new Map();
|
|
124
|
-
for (const entry of entries) {
|
|
125
|
-
const id = toolIdentifier(entry?.id);
|
|
126
|
-
if (id !== null && !unique.has(id)) unique.set(id, { ...entry, id });
|
|
127
|
-
}
|
|
128
|
-
return [...unique.values()];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function structuredDelegatedInputs(result) {
|
|
132
|
-
const parsed = parseToolResult(result, "read_thread result");
|
|
133
|
-
const inputs = [];
|
|
134
|
-
for (const turn of parsed.turns ?? []) {
|
|
135
|
-
for (const item of turn?.items ?? []) {
|
|
136
|
-
if (item?.type !== "userMessage") continue;
|
|
137
|
-
for (const part of item.content ?? []) {
|
|
138
|
-
if (typeof part?.codexDelegation?.input === "string") {
|
|
139
|
-
inputs.push(part.codexDelegation.input);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return inputs;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function hasExactTaskChefMarker(result, taskId) {
|
|
148
|
-
const id = requireUuid(taskId);
|
|
149
|
-
return structuredDelegatedInputs(result).some(
|
|
150
|
-
(input) => parseTaskChefMarker(input) === id,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function filterThreadCandidates(result, {
|
|
155
|
-
baselineThreadIds = new Set(),
|
|
156
|
-
excludedThreadIds = new Set(),
|
|
157
|
-
hostId = null,
|
|
158
|
-
projectId = null,
|
|
159
|
-
title = null,
|
|
160
|
-
createdAfter = null,
|
|
161
|
-
environmentType = null,
|
|
162
|
-
} = {}) {
|
|
163
|
-
const minimumCreatedAt = createdAfter === null
|
|
164
|
-
? null
|
|
165
|
-
: timestampMilliseconds(createdAfter) - THREAD_RESOLUTION_CLOCK_SKEW_MS;
|
|
166
|
-
const candidates = listThreadEntries(result).filter((thread) => {
|
|
167
|
-
if (
|
|
168
|
-
baselineThreadIds.has(thread.id)
|
|
169
|
-
|| excludedThreadIds.has(thread.id)
|
|
170
|
-
|| isProvisionalThreadId(thread.id)
|
|
171
|
-
|| thread.kind !== "codex"
|
|
172
|
-
) return false;
|
|
173
|
-
if (hostId !== null && thread.hostId !== undefined && thread.hostId !== hostId) return false;
|
|
174
|
-
if (projectId !== null && thread.projectId !== undefined && thread.projectId !== projectId) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
const candidateEnvironmentType = thread.environment?.type
|
|
178
|
-
?? thread.environmentType
|
|
179
|
-
?? thread.backing?.environment?.type;
|
|
180
|
-
if (
|
|
181
|
-
environmentType !== null
|
|
182
|
-
&& candidateEnvironmentType !== undefined
|
|
183
|
-
&& candidateEnvironmentType !== environmentType
|
|
184
|
-
) return false;
|
|
185
|
-
const candidateTime = timestampMilliseconds(thread.createdAt ?? thread.updatedAt);
|
|
186
|
-
if (minimumCreatedAt !== null && candidateTime !== null && candidateTime < minimumCreatedAt) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
return true;
|
|
190
|
-
});
|
|
191
|
-
if (title === null) return candidates;
|
|
192
|
-
return candidates.sort((left, right) =>
|
|
193
|
-
Number(right.title === title) - Number(left.title === title));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
107
|
export async function createAndRecordDelegation(input) {
|
|
197
|
-
const {
|
|
198
|
-
project,
|
|
199
|
-
title,
|
|
200
|
-
instruction,
|
|
201
|
-
target,
|
|
202
|
-
createThread,
|
|
203
|
-
recordTask,
|
|
204
|
-
resolveRecordedTask = null,
|
|
205
|
-
reportRecordedResult = null,
|
|
206
|
-
taskId = randomUUID(),
|
|
207
|
-
} = input ?? {};
|
|
108
|
+
const { project, title, instruction, target, createThread, recordTask, reportRecordedResult = null, taskId = randomUUID() } = input ?? {};
|
|
208
109
|
requireString(project, "project");
|
|
209
110
|
requireString(title, "title");
|
|
210
111
|
requireObject(target, "target");
|
|
211
|
-
for (const [value, name] of [
|
|
212
|
-
[createThread, "createThread"],
|
|
213
|
-
[recordTask, "recordTask"],
|
|
214
|
-
]) {
|
|
112
|
+
for (const [value, name] of [[createThread, "createThread"], [recordTask, "recordTask"]]) {
|
|
215
113
|
if (typeof value !== "function") throw new Error(`${name} must be a function`);
|
|
216
114
|
}
|
|
217
|
-
|
|
218
|
-
[resolveRecordedTask, "resolveRecordedTask"],
|
|
219
|
-
[reportRecordedResult, "reportRecordedResult"],
|
|
220
|
-
]) {
|
|
221
|
-
if (value !== null && typeof value !== "function") {
|
|
222
|
-
throw new Error(`${name} must be a function or null`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
115
|
+
if (reportRecordedResult !== null && typeof reportRecordedResult !== "function") throw new Error("reportRecordedResult must be a function or null");
|
|
225
116
|
|
|
226
117
|
const prepared = prepareDelegation(instruction, { taskId });
|
|
227
|
-
await recordTask({
|
|
228
|
-
id: prepared.id,
|
|
229
|
-
project,
|
|
230
|
-
title,
|
|
231
|
-
instruction: prepared.instruction,
|
|
232
|
-
threadId: null,
|
|
233
|
-
});
|
|
118
|
+
await recordTask({ id: prepared.id, project, title, instruction: prepared.instruction, threadId: null });
|
|
234
119
|
|
|
235
120
|
let createResult;
|
|
236
121
|
try {
|
|
237
|
-
createResult = parseToolResult(await createThread({
|
|
238
|
-
prompt: prepared.instruction,
|
|
239
|
-
title,
|
|
240
|
-
target,
|
|
241
|
-
}), "create_thread result");
|
|
122
|
+
createResult = parseToolResult(await createThread({ prompt: prepared.instruction, title, target }), "create_thread result");
|
|
242
123
|
} catch (error) {
|
|
243
124
|
let resultReporting = "unavailable";
|
|
244
125
|
if (reportRecordedResult !== null) {
|
|
245
126
|
try {
|
|
246
|
-
await reportRecordedResult({
|
|
247
|
-
taskId: prepared.id,
|
|
248
|
-
threadId: null,
|
|
249
|
-
turnId: null,
|
|
250
|
-
status: "failed",
|
|
251
|
-
summary: "Executor creation failed before the executor started.",
|
|
252
|
-
});
|
|
127
|
+
await reportRecordedResult({ taskId: prepared.id, threadId: null, turnId: null, status: "failed", summary: "Executor creation failed before the executor started." });
|
|
253
128
|
resultReporting = "recorded";
|
|
254
129
|
} catch {
|
|
255
130
|
resultReporting = "failed";
|
|
@@ -261,58 +136,6 @@ export async function createAndRecordDelegation(input) {
|
|
|
261
136
|
const returnedThreadId = toolIdentifier(createResult.threadId);
|
|
262
137
|
const clientThreadId = toolIdentifier(createResult.clientThreadId);
|
|
263
138
|
const pendingWorktreeId = toolIdentifier(createResult.pendingWorktreeId);
|
|
264
|
-
const provisional = clientThreadId
|
|
265
|
-
|
|
266
|
-
?? (isProvisionalThreadId(returnedThreadId) ? returnedThreadId : null);
|
|
267
|
-
const durableThreadId = returnedThreadId !== null
|
|
268
|
-
&& !isProvisionalThreadId(returnedThreadId)
|
|
269
|
-
&& returnedThreadId !== clientThreadId
|
|
270
|
-
&& returnedThreadId !== pendingWorktreeId
|
|
271
|
-
? returnedThreadId
|
|
272
|
-
: null;
|
|
273
|
-
|
|
274
|
-
if (durableThreadId !== null) {
|
|
275
|
-
if (resolveRecordedTask !== null) {
|
|
276
|
-
try {
|
|
277
|
-
await resolveRecordedTask({ id: prepared.id, threadId: durableThreadId });
|
|
278
|
-
return {
|
|
279
|
-
status: "recorded",
|
|
280
|
-
resolution: "immediate",
|
|
281
|
-
...prepared,
|
|
282
|
-
threadId: durableThreadId,
|
|
283
|
-
provisional,
|
|
284
|
-
hostId: createResult.hostId ?? null,
|
|
285
|
-
};
|
|
286
|
-
} catch (error) {
|
|
287
|
-
return {
|
|
288
|
-
status: "recorded-unresolved",
|
|
289
|
-
reason: "task-resolution-failed",
|
|
290
|
-
resolutionError: error instanceof Error ? error.message : String(error),
|
|
291
|
-
...prepared,
|
|
292
|
-
threadId: null,
|
|
293
|
-
createdThreadId: durableThreadId,
|
|
294
|
-
provisional,
|
|
295
|
-
hostId: createResult.hostId ?? null,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return {
|
|
300
|
-
status: "recorded-unresolved",
|
|
301
|
-
reason: "task-resolution-unavailable",
|
|
302
|
-
...prepared,
|
|
303
|
-
threadId: null,
|
|
304
|
-
createdThreadId: durableThreadId,
|
|
305
|
-
provisional,
|
|
306
|
-
hostId: createResult.hostId ?? null,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
status: "recorded-unresolved",
|
|
312
|
-
reason: "awaiting-bounded-marker-resolution",
|
|
313
|
-
...prepared,
|
|
314
|
-
threadId: null,
|
|
315
|
-
provisional,
|
|
316
|
-
hostId: createResult.hostId ?? null,
|
|
317
|
-
};
|
|
139
|
+
const provisional = clientThreadId ?? pendingWorktreeId ?? (isProvisionalThreadId(returnedThreadId) ? returnedThreadId : null);
|
|
140
|
+
return { status: "recorded-link-pending", resolution: "executor-self-link", ...prepared, threadId: null, provisional, hostId: createResult.hostId ?? null };
|
|
318
141
|
}
|
package/src/mcp.js
CHANGED
|
@@ -2,9 +2,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
prepareDispatch,
|
|
5
|
+
linkTask,
|
|
5
6
|
recordTask,
|
|
6
7
|
reportTaskResult,
|
|
7
|
-
resolveTask,
|
|
8
8
|
} from "./workspace.js";
|
|
9
9
|
import { parseTaskChefMarker } from "./delegation.js";
|
|
10
10
|
import { resolveWorkspacePath } from "./workspace-path.js";
|
|
@@ -54,13 +54,13 @@ export function createTaskChefMcpServer({
|
|
|
54
54
|
prepare = prepareDispatch,
|
|
55
55
|
record = recordTask,
|
|
56
56
|
reportResult = reportTaskResult,
|
|
57
|
-
|
|
57
|
+
link = linkTask,
|
|
58
58
|
} = {}) {
|
|
59
59
|
const server = new McpServer(
|
|
60
60
|
{ name: "taskchef", version: "1.0.0" },
|
|
61
61
|
{
|
|
62
62
|
instructions:
|
|
63
|
-
"Prepare with prepare_dispatch, call record_task before creating the Codex task, then create it natively
|
|
63
|
+
"Prepare with prepare_dispatch, call record_task before creating the Codex task, then create it natively and return immediately. The executor must call link_task before other work and report_result before ending.",
|
|
64
64
|
},
|
|
65
65
|
);
|
|
66
66
|
|
|
@@ -89,13 +89,13 @@ export function createTaskChefMcpServer({
|
|
|
89
89
|
{
|
|
90
90
|
title: "Record TaskChef task",
|
|
91
91
|
description:
|
|
92
|
-
"Atomically append one prepared TaskChef task before creating its Codex executor. Pass the exact marked instruction and
|
|
92
|
+
"Atomically append one prepared TaskChef task before creating its Codex executor. Pass the exact marked instruction and null threadId; only the executor may self-link it.",
|
|
93
93
|
inputSchema: {
|
|
94
94
|
id: z.string().min(1),
|
|
95
95
|
project: z.string().min(1),
|
|
96
96
|
title: z.string().min(1),
|
|
97
97
|
instruction: z.string().min(1),
|
|
98
|
-
threadId: z.
|
|
98
|
+
threadId: z.null(),
|
|
99
99
|
},
|
|
100
100
|
outputSchema: { task: taskSchema },
|
|
101
101
|
annotations: {
|
|
@@ -114,11 +114,11 @@ export function createTaskChefMcpServer({
|
|
|
114
114
|
);
|
|
115
115
|
|
|
116
116
|
server.registerTool(
|
|
117
|
-
"
|
|
117
|
+
"link_task",
|
|
118
118
|
{
|
|
119
|
-
title: "
|
|
119
|
+
title: "Link TaskChef executor",
|
|
120
120
|
description:
|
|
121
|
-
"
|
|
121
|
+
"Register this executor's asserted durable Codex thread ID. The atomic one-way transition is idempotent and rejects conflicts or thread reuse.",
|
|
122
122
|
inputSchema: {
|
|
123
123
|
taskId: z.string().min(1),
|
|
124
124
|
threadId: z.string().min(1),
|
|
@@ -131,8 +131,8 @@ export function createTaskChefMcpServer({
|
|
|
131
131
|
},
|
|
132
132
|
},
|
|
133
133
|
async ({ taskId, threadId }) => {
|
|
134
|
-
const task = await
|
|
135
|
-
return toolResult("task", task, `
|
|
134
|
+
const task = await link(workspace, taskId, threadId);
|
|
135
|
+
return toolResult("task", task, `Linked TaskChef task ${task.id}.`);
|
|
136
136
|
},
|
|
137
137
|
);
|
|
138
138
|
|