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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
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 includes a unique `# taskchef_id=<UUID>` marker.
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 show one row per configured GitHub repository.
285
- Project details repeat on each row so multi-repository projects remain clear.
286
- A project without a configured repository has one row containing `-` in the
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
- payments git https://github.com/example/payments-sdk /workspace/payments
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":"# taskchef_id=c0f010ff-84f2-4838-a69d-0ff1f5d721d7\n\nAdd structured logs for failed retries and test them.","threadId":"019f..."}' |
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":"# 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"}
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
- `# taskchef_id=<full UUID>` correlation marker.
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. TaskChef does not eagerly rewrite
169
- legacy history solely for this migration.
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
- `# taskchef_id=<UUID>` marker
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -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 `# taskchef_id=<full UUID>`, followed by a
51
- blank line and the instruction body. Preserve this marked instruction for
52
- recording, and note the creation time. Do not take a pre-creation thread
53
- snapshot; the exact random marker is the correlation key.
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 `# taskchef_id=<full UUID>` marker and exactly one candidate
89
- matches. Apply the same marker verification to a thread ID returned by a
90
- native resolver. Reject any returned or discovered thread ID equal to the
91
- provisional identifier or in its `local:` namespace. Then use
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. When exactly one candidate
33
- starts with the task's exact marker, run
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) => value === null || value === undefined || value === ""
101
- ? "-"
102
- : String(value);
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 repositories = project.githubRepos.length > 0 ? project.githubRepos : [null];
114
- return repositories.map((repository) => [
117
+ const [primaryRepository = null, ...additionalRepositories] = project.githubRepos;
118
+ return [[
115
119
  project.name,
116
120
  project.isGitRepository ? "git" : "folder",
117
- repository,
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 UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
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 `# taskchef_id=${requireUuid(taskId)}`;
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 match = firstLine.match(/^# taskchef_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/);
77
- return match?.[1] ?? null;
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 marker = taskChefMarker(taskId);
130
+ const id = requireUuid(taskId);
123
131
  return structuredDelegatedInputs(result).some(
124
- (input) => input.split(/\r?\n/, 1)[0] === marker,
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(dispatch, name = "task") {
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
  }