taskchef 5.0.0 → 5.0.2
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/README.md +9 -7
- package/SPEC.md +12 -5
- package/package.json +1 -1
- package/skills/taskchef-delegate/SKILL.md +12 -8
- package/skills/taskchef-report/SKILL.md +4 -2
- package/src/cli.js +16 -7
- package/src/delegation.js +15 -7
- package/src/workspace.js +12 -6
package/README.md
CHANGED
|
@@ -119,7 +119,9 @@ an unresolved task's nullable thread ID. The log records what TaskChef sent,
|
|
|
119
119
|
when it sent it, which project it selected, and which Codex task received the
|
|
120
120
|
work.
|
|
121
121
|
|
|
122
|
-
Every delegated instruction
|
|
122
|
+
Every delegated instruction begins with a unique
|
|
123
|
+
`<!-- taskchef_id=<UUID> -->` marker followed by a blank line. The valid HTML
|
|
124
|
+
comment stays invisible in rendered Markdown.
|
|
123
125
|
If worktree creation does not return a thread ID immediately, TaskChef records
|
|
124
126
|
the marked delegation as unresolved, then waits briefly for the durable task.
|
|
125
127
|
It prefers a native Codex client-ID resolver when available and otherwise makes
|
|
@@ -281,16 +283,16 @@ taskchef project list
|
|
|
281
283
|
taskchef project remove payments
|
|
282
284
|
```
|
|
283
285
|
|
|
284
|
-
Human-readable project listings
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
repository column:
|
|
286
|
+
Human-readable project listings group configured GitHub repositories by
|
|
287
|
+
project. The first row shows the project details; additional repository rows
|
|
288
|
+
leave the repeated name, kind, and path columns blank. A project without a
|
|
289
|
+
configured repository has one row containing `-` in the repository column:
|
|
288
290
|
|
|
289
291
|
```text
|
|
290
292
|
NAME KIND GITHUB REPOSITORY PATH
|
|
291
293
|
notes folder - /workspace/notes
|
|
292
294
|
payments git https://github.com/example/payments-api /workspace/payments
|
|
293
|
-
|
|
295
|
+
https://github.com/example/payments-sdk
|
|
294
296
|
```
|
|
295
297
|
|
|
296
298
|
Import projects as a JSON array from a file or standard input:
|
|
@@ -316,7 +318,7 @@ task lines remain readable without an eager rewrite of the append-only history.
|
|
|
316
318
|
`project` value is the exact configured project path:
|
|
317
319
|
|
|
318
320
|
```sh
|
|
319
|
-
printf '%s\n' '{"id":"c0f010ff-84f2-4838-a69d-0ff1f5d721d7","project":"/workspace/payments","title":"Add retry logs","instruction":"
|
|
321
|
+
printf '%s\n' '{"id":"c0f010ff-84f2-4838-a69d-0ff1f5d721d7","project":"/workspace/payments","title":"Add retry logs","instruction":"<!-- taskchef_id=c0f010ff-84f2-4838-a69d-0ff1f5d721d7 -->\n\nAdd structured logs for failed retries and test them.","threadId":"019f..."}' |
|
|
320
322
|
taskchef task record --json
|
|
321
323
|
```
|
|
322
324
|
|
package/SPEC.md
CHANGED
|
@@ -134,7 +134,7 @@ schedules, task status, results, host information, or the workspace path.
|
|
|
134
134
|
`tasks.jsonl` contains one compact JSON object per line, in append order:
|
|
135
135
|
|
|
136
136
|
```json
|
|
137
|
-
{"schemaVersion":2,"id":"c0f010ff-84f2-4838-a69d-0ff1f5d721d7","project":{"name":"payments-api","path":"/workspace/payments-api","isGitRepository":true,"githubRepos":["https://github.com/example/payments-api","https://github.com/example/payments-sdk"],"description":"Owns payment authorization, capture, refunds, and provider integrations."},"title":"Add payment retry logs","instruction":"
|
|
137
|
+
{"schemaVersion":2,"id":"c0f010ff-84f2-4838-a69d-0ff1f5d721d7","project":{"name":"payments-api","path":"/workspace/payments-api","isGitRepository":true,"githubRepos":["https://github.com/example/payments-api","https://github.com/example/payments-sdk"],"description":"Owns payment authorization, capture, refunds, and provider integrations."},"title":"Add payment retry logs","instruction":"<!-- taskchef_id=c0f010ff-84f2-4838-a69d-0ff1f5d721d7 -->\n\nAdd structured logs for failed payment retries and test them.","threadId":"019f9d46-f42c-7482-9707-3c107bf241ee","createdAt":"2026-08-08T10:00:00.000Z"}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
- `schemaVersion` identifies the task entry format.
|
|
@@ -142,7 +142,8 @@ schedules, task status, results, host information, or the workspace path.
|
|
|
142
142
|
- `project` is the complete configured project snapshot used for routing.
|
|
143
143
|
- `title` is a short task name.
|
|
144
144
|
- `instruction` is the complete executor instruction, including its first-line
|
|
145
|
-
|
|
145
|
+
`<!-- taskchef_id=<full UUID> -->` correlation marker and the blank line that
|
|
146
|
+
follows it.
|
|
146
147
|
- `threadId` identifies the created Codex task, or is `null` when creation was
|
|
147
148
|
accepted but bounded marker resolution did not find one durable task ID.
|
|
148
149
|
- `createdAt` is the dispatch time as an ISO 8601 timestamp.
|
|
@@ -165,8 +166,13 @@ hidden reasoning, `hostId`, or update timestamps.
|
|
|
165
166
|
|
|
166
167
|
New task entries use schema version 2 and list-valued project snapshots.
|
|
167
168
|
Version 1 entries with string or null repository metadata remain readable and
|
|
168
|
-
normalize to version 2 in API and CLI output.
|
|
169
|
-
|
|
169
|
+
normalize to version 2 in API and CLI output. Historical task entries with the
|
|
170
|
+
old heading-style marker also remain readable. New nullable records, candidate
|
|
171
|
+
matching, and resolution require the exact HTML-comment marker; TaskChef never
|
|
172
|
+
uses an old marker to correlate a thread. Direct records that already have a
|
|
173
|
+
durable thread ID remain marker-independent because they do not use recovery.
|
|
174
|
+
TaskChef does not eagerly rewrite legacy history solely for these compatibility
|
|
175
|
+
cases.
|
|
170
176
|
|
|
171
177
|
## Dispatch workflow
|
|
172
178
|
|
|
@@ -175,7 +181,8 @@ For each assignment, `$taskchef-delegate`:
|
|
|
175
181
|
1. loads and validates configured projects
|
|
176
182
|
2. selects one unambiguous target
|
|
177
183
|
3. generates a full UUID and prefixes the instruction with its exact
|
|
178
|
-
|
|
184
|
+
`<!-- taskchef_id=<UUID> -->` marker as the first line, followed by a blank
|
|
185
|
+
line
|
|
179
186
|
4. creates a real Codex task at the exact configured path
|
|
180
187
|
5. appends a task entry immediately when creation returns a durable thread ID
|
|
181
188
|
6. when creation returns only a provisional client ID, immediately appends the
|
package/package.json
CHANGED
|
@@ -47,10 +47,11 @@ for all deterministic workspace and task-record operations.
|
|
|
47
47
|
matches. Ask instead of guessing when no project or several projects match.
|
|
48
48
|
4. Resolve native projects once and require the exact configured path.
|
|
49
49
|
5. Generate a lowercase full UUID task ID before creation. Prefix the complete
|
|
50
|
-
executor instruction with exactly
|
|
51
|
-
blank line and the instruction body. Preserve this
|
|
52
|
-
recording, and note the creation time.
|
|
53
|
-
snapshot; the exact random marker is the
|
|
50
|
+
executor instruction with exactly `<!-- taskchef_id=<full UUID> -->` as the
|
|
51
|
+
first line, followed by a blank line and the instruction body. Preserve this
|
|
52
|
+
marked instruction for recording, and note the creation time.
|
|
53
|
+
Do not take a pre-creation thread snapshot; the exact random marker is the
|
|
54
|
+
correlation key.
|
|
54
55
|
6. Create one real Codex task using the exact configured project, a local
|
|
55
56
|
environment on its executor host, the marked instruction, and a short title.
|
|
56
57
|
7. When `create_thread` returns a durable `threadId`, immediately run
|
|
@@ -85,10 +86,13 @@ for all deterministic workspace and task-record operations.
|
|
|
85
86
|
`userMessage.content[].codexDelegation.input`; do not trust titles,
|
|
86
87
|
summaries, previews, plain-text echoes, or assistant output as proof.
|
|
87
88
|
- Accept a candidate only when the structured input's first line is exactly
|
|
88
|
-
the task's
|
|
89
|
-
matches.
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
the task's `<!-- taskchef_id=<full UUID> -->` marker and exactly one
|
|
90
|
+
candidate matches. Require an immediately following blank line. Reject old
|
|
91
|
+
heading-style markers, malformed comments, missing blank separators, and
|
|
92
|
+
marker-like text anywhere else. Apply the same marker verification to a
|
|
93
|
+
thread ID returned by a native resolver. Reject any returned or discovered
|
|
94
|
+
thread ID equal to the provisional identifier or in its `local:` namespace.
|
|
95
|
+
Then use
|
|
92
96
|
the task-resolution command under **Later resolution** to atomically fill
|
|
93
97
|
the nullable field.
|
|
94
98
|
- Treat native-resolution, snapshot, candidate-read, wait, and task-resolution
|
|
@@ -29,8 +29,10 @@ all deterministic task-log operations.
|
|
|
29
29
|
2. Separate entries whose `threadId` is `null`. For those entries, take one
|
|
30
30
|
`list_threads` snapshot with limit 50, filter by available project metadata,
|
|
31
31
|
and inspect candidate structured delegated inputs. Use title only to
|
|
32
|
-
prioritize candidates, never to exclude them.
|
|
33
|
-
|
|
32
|
+
prioritize candidates, never to exclude them. Require the exact first line
|
|
33
|
+
`<!-- taskchef_id=<full lowercase UUID> -->` and an immediately following
|
|
34
|
+
blank line; reject old heading-style markers, malformed comments, and missing
|
|
35
|
+
blank separators. When exactly one candidate has that exact prefix, run
|
|
34
36
|
`<plugin-root>/bin/taskchef.js task resolve <task-id> --thread-id <thread-id> --json`.
|
|
35
37
|
Do not resolve zero or multiple matches. Report unmatched entries as
|
|
36
38
|
recorded but unresolved and do not pass them to native thread tools.
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
resolveTask,
|
|
19
19
|
} from "./workspace.js";
|
|
20
20
|
|
|
21
|
+
const BLANK_TABLE_CELL = Symbol("blank table cell");
|
|
22
|
+
|
|
21
23
|
async function readStdin() {
|
|
22
24
|
let input = "";
|
|
23
25
|
process.stdin.setEncoding("utf8");
|
|
@@ -97,9 +99,11 @@ function print(value, args, human) {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
function table(headers, rows) {
|
|
100
|
-
const display = (value) =>
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
const display = (value) => {
|
|
103
|
+
if (value === BLANK_TABLE_CELL) return "";
|
|
104
|
+
if (value === null || value === undefined || value === "") return "-";
|
|
105
|
+
return String(value);
|
|
106
|
+
};
|
|
103
107
|
const widths = headers.map((header, index) =>
|
|
104
108
|
Math.max(header.length, ...rows.map((row) => display(row[index]).length)));
|
|
105
109
|
const format = (row) => row.map((value, index) => index === row.length - 1
|
|
@@ -110,13 +114,18 @@ function table(headers, rows) {
|
|
|
110
114
|
|
|
111
115
|
function projectRows(projects) {
|
|
112
116
|
return projects.flatMap((project) => {
|
|
113
|
-
const
|
|
114
|
-
return
|
|
117
|
+
const [primaryRepository = null, ...additionalRepositories] = project.githubRepos;
|
|
118
|
+
return [[
|
|
115
119
|
project.name,
|
|
116
120
|
project.isGitRepository ? "git" : "folder",
|
|
117
|
-
|
|
121
|
+
primaryRepository,
|
|
118
122
|
project.path,
|
|
119
|
-
])
|
|
123
|
+
], ...additionalRepositories.map((repository) => [
|
|
124
|
+
BLANK_TABLE_CELL,
|
|
125
|
+
BLANK_TABLE_CELL,
|
|
126
|
+
repository,
|
|
127
|
+
BLANK_TABLE_CELL,
|
|
128
|
+
])];
|
|
120
129
|
});
|
|
121
130
|
}
|
|
122
131
|
|
package/src/delegation.js
CHANGED
|
@@ -5,7 +5,10 @@ export const THREAD_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
|
5
5
|
export const THREAD_RESOLUTION_RECENT_LIMIT = 50;
|
|
6
6
|
export const THREAD_RESOLUTION_CLOCK_SKEW_MS = 5_000;
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const UUID_SOURCE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
9
|
+
const UUID_PATTERN = new RegExp(`^${UUID_SOURCE}$`);
|
|
10
|
+
const TASKCHEF_MARKER_PATTERN = new RegExp(`^<!-- taskchef_id=(${UUID_SOURCE}) -->$`);
|
|
11
|
+
const LEGACY_TASKCHEF_MARKER_PATTERN = new RegExp(`^# taskchef_id=(${UUID_SOURCE})$`);
|
|
9
12
|
|
|
10
13
|
function requireObject(value, name) {
|
|
11
14
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -67,14 +70,19 @@ function wait(delayMs) {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export function taskChefMarker(taskId) {
|
|
70
|
-
return
|
|
73
|
+
return `<!-- taskchef_id=${requireUuid(taskId)} -->`;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
export function parseTaskChefMarker(instruction) {
|
|
76
|
+
export function parseTaskChefMarker(instruction, { allowLegacyHeading = false } = {}) {
|
|
74
77
|
if (typeof instruction !== "string") return null;
|
|
75
78
|
const firstLine = instruction.split(/\r?\n/, 1)[0];
|
|
76
|
-
const
|
|
77
|
-
|
|
79
|
+
const currentMatch = firstLine.match(TASKCHEF_MARKER_PATTERN);
|
|
80
|
+
if (currentMatch !== null) {
|
|
81
|
+
const prefix = instruction.match(/^([^\r\n]*)(\r?\n)\2/);
|
|
82
|
+
return prefix === null ? null : currentMatch[1];
|
|
83
|
+
}
|
|
84
|
+
if (!allowLegacyHeading) return null;
|
|
85
|
+
return firstLine.match(LEGACY_TASKCHEF_MARKER_PATTERN)?.[1] ?? null;
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
@@ -119,9 +127,9 @@ export function structuredDelegatedInputs(result) {
|
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
export function hasExactTaskChefMarker(result, taskId) {
|
|
122
|
-
const
|
|
130
|
+
const id = requireUuid(taskId);
|
|
123
131
|
return structuredDelegatedInputs(result).some(
|
|
124
|
-
(input) => input
|
|
132
|
+
(input) => parseTaskChefMarker(input) === id,
|
|
125
133
|
);
|
|
126
134
|
}
|
|
127
135
|
|
package/src/workspace.js
CHANGED
|
@@ -701,7 +701,11 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
701
701
|
});
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
-
async function validateDispatchShape(
|
|
704
|
+
async function validateDispatchShape(
|
|
705
|
+
dispatch,
|
|
706
|
+
name = "task",
|
|
707
|
+
{ allowLegacyHeading = false } = {},
|
|
708
|
+
) {
|
|
705
709
|
requireExactFields(dispatch, DISPATCH_FIELDS, name);
|
|
706
710
|
if (![LEGACY_SCHEMA_VERSION, CURRENT_SCHEMA_VERSION].includes(dispatch.schemaVersion)) {
|
|
707
711
|
throw new Error(`unsupported ${name} schemaVersion`);
|
|
@@ -724,7 +728,7 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
724
728
|
};
|
|
725
729
|
if (
|
|
726
730
|
normalized.threadId === null &&
|
|
727
|
-
parseTaskChefMarker(normalized.instruction) !== normalized.id
|
|
731
|
+
parseTaskChefMarker(normalized.instruction, { allowLegacyHeading }) !== normalized.id
|
|
728
732
|
) {
|
|
729
733
|
throw new Error(`${name} with a null threadId must contain its exact TaskChef marker`);
|
|
730
734
|
}
|
|
@@ -756,7 +760,9 @@ async function readDispatchRecordsUnlocked(root) {
|
|
|
756
760
|
records.push({
|
|
757
761
|
line,
|
|
758
762
|
raw: value,
|
|
759
|
-
normalized: await validateDispatchShape(value, `task line ${index + 1}
|
|
763
|
+
normalized: await validateDispatchShape(value, `task line ${index + 1}`, {
|
|
764
|
+
allowLegacyHeading: true,
|
|
765
|
+
}),
|
|
760
766
|
});
|
|
761
767
|
}
|
|
762
768
|
const ids = new Set();
|
|
@@ -823,13 +829,13 @@ export async function resolveTask(workspaceRoot, taskId, threadId) {
|
|
|
823
829
|
const index = dispatches.findIndex((dispatch) => dispatch.id === id);
|
|
824
830
|
if (index === -1) throw new Error(`task not found: ${id}`);
|
|
825
831
|
const dispatch = dispatches[index];
|
|
826
|
-
if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
|
|
827
|
-
throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
|
|
828
|
-
}
|
|
829
832
|
if (dispatch.threadId === durableThreadId) return dispatch;
|
|
830
833
|
if (dispatch.threadId !== null) {
|
|
831
834
|
throw new Error(`task already has a different threadId: ${id}`);
|
|
832
835
|
}
|
|
836
|
+
if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
|
|
837
|
+
throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
|
|
838
|
+
}
|
|
833
839
|
if (dispatches.some((item) => item.threadId === durableThreadId)) {
|
|
834
840
|
throw new Error(`threadId is already recorded: ${durableThreadId}`);
|
|
835
841
|
}
|