taskchef 5.10.0 → 5.11.1

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.10.0",
3
+ "version": "5.11.1",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/BACKLOG.md CHANGED
@@ -50,7 +50,8 @@ clear data model before implementation.
50
50
  `resolve_client_thread(clientThreadId) -> { status, threadId? }`. Returning a
51
51
  reserved durable ID from `create_thread`, or emitting a materialization event
52
52
  containing it, would also close the lifecycle gap.
53
- - Re-evaluate whether the initial-hook identity link and exact-marker manual
54
- recovery can be simplified when Codex exposes one of these APIs. Keep exact
55
- correlation verification before persisting the returned durable ID unless
56
- the official contract provides equivalent guarantees.
53
+ - Replace bounded exact-marker discovery when Codex exposes one of these APIs.
54
+ The initial hook intentionally never links from `session_id`, because Codex
55
+ documents that subagent hooks use the parent session ID. Keep exact
56
+ correlation verification unless an official contract provides equivalent
57
+ guarantees.
package/README.md CHANGED
@@ -140,9 +140,12 @@ Every delegated instruction begins with a unique
140
140
  executor-ownership paragraph, a result-callback paragraph, and the assignment. The
141
141
  valid HTML comment stays invisible in rendered Markdown.
142
142
  TaskChef records the marked delegation before executor creation. If creation
143
- does not return a durable thread ID, the trusted initial-prompt hook receives
144
- the root session ID and atomically links it to the existing entry. Delegation
145
- does not wait, poll, list tasks, or retry discovery.
143
+ does not return a durable thread ID, the dispatcher performs bounded checks at
144
+ 10 and 30 seconds and accepts only one recently created task whose structured
145
+ initial input contains the exact marker. The initial-prompt hook may wait for
146
+ that verified link, but never treats its session ID as executor identity:
147
+ subagent hooks can carry the parent session ID. If no unique match appears,
148
+ TaskChef leaves the entry unresolved instead of risking a wrong link.
146
149
 
147
150
  ### Ask for a live report
148
151
 
@@ -433,11 +436,10 @@ printf '%s\n' '{"id":"c0f010ff-84f2-4838-a69d-0ff1f5d721d7","project":"/workspac
433
436
  taskchef task record --json
434
437
  ```
435
438
 
436
- If a task has `threadId: null`, the initial plugin hook normally resolves its
437
- exact marker to the root session ID. Direct manual recovery may use the
438
- equivalent CLI operation below after verifying one exact structured marker
439
- match. Both hook and CLI reach the same locked atomic logic, which permits only
440
- the one-way transition from null to one unique thread ID:
439
+ If a task still has `threadId: null` after bounded resolution, direct manual
440
+ recovery may use the CLI operation below after verifying one exact structured
441
+ marker match. The same locked atomic logic permits only the one-way transition
442
+ from null to one unique thread ID:
441
443
 
442
444
  ```sh
443
445
  taskchef task resolve c0f010ff-84f2-4838-a69d-0ff1f5d721d7 \
package/SPEC.md CHANGED
@@ -22,8 +22,8 @@ latest useful semantic result, not a second lifecycle or event database.
22
22
  4. It embeds a generated TaskChef UUID marker, executor-ownership sentence, and
23
23
  result-callback instruction, then records the task with `threadId: null`.
24
24
  5. It creates an independently openable Codex task. A durable returned ID is
25
- resolved immediately; otherwise the initial-prompt hook links the root
26
- session ID without polling.
25
+ resolved immediately; otherwise bounded recent-task checks accept exactly
26
+ one child whose structured initial input contains the exact marker.
27
27
  6. It returns without waiting for executor work to complete.
28
28
  7. Executors report `completed`, `needs_input`, or `failed` through MCP. Reports
29
29
  filter old terminal entries, use one live metadata snapshot, and read only
@@ -217,15 +217,17 @@ For each assignment, `$taskchef-delegate`:
217
217
  before executor creation
218
218
  5. creates a real Codex task at the exact configured path
219
219
  6. calls `resolve_task` when creation returns a durable thread ID; otherwise it
220
- returns immediately and the initial `UserPromptSubmit` hook resolves the root
221
- session ID against the exact marker
220
+ checks at 10 and 30 seconds and resolves only one recent task whose
221
+ structured initial input contains the exact marker
222
222
  7. records executor-creation failure as a `failed` semantic result on the
223
223
  already-existing entry
224
- 8. never lists, reads, waits for, or polls threads during delegation.
224
+ 8. returns after the bounded identity check without waiting for executor work.
225
225
 
226
226
  Recording first closes the creation/hook race. On the exact marked prompt, the
227
- hook receives the root session ID and initial turn ID and writes only identity
228
- plus `working`. On later prompts whose session ID matches a recorded executor,
227
+ hook waits for the separately verified canonical identity, then writes only the
228
+ initial turn plus `working`. It never persists the hook `session_id`, because a
229
+ subagent hook can receive its parent's session ID. On later prompts whose
230
+ session ID matches a recorded executor,
229
231
  the same `UserPromptSubmit` hook reads the task history and supplies the current
230
232
  turn ID as callback context without writing. No permission, tool, stop,
231
233
  notification, or session hook writes task lifecycle state. A provisional
@@ -369,7 +371,7 @@ TaskChef does not include:
369
371
  2. Project metadata routes an unambiguous request to the correct local project.
370
372
  3. A delegation records `working` before creating a visible Codex task.
371
373
  4. A durable creation resolves immediately; a provisional creation is linked
372
- by the initial exact-marker hook without polling.
374
+ only after bounded discovery verifies one exact structured-marker match.
373
375
  5. The dispatcher returns without waiting for execution.
374
376
  6. Several independent assignments can create several entries, including
375
377
  multiple entries for the same project.
@@ -27,8 +27,11 @@ sequenceDiagram
27
27
  alt Durable ID returned
28
28
  D->>M: resolve_task
29
29
  else Provisional ID returned
30
+ D->>C: Bounded recent-task checks
31
+ D->>C: Read candidates and verify exact marker
32
+ D->>M: resolve_task(verified child ID)
30
33
  C->>H: Initial UserPromptSubmit
31
- H->>W: Resolve root session ID and initial turn
34
+ H->>W: Wait for verified link; record initial turn
32
35
  end
33
36
  D-->>U: Return immediately
34
37
  C->>M: report_result(needs_input | completed | failed)
@@ -37,8 +40,8 @@ sequenceDiagram
37
40
 
38
41
  Recording happens before creation. This closes the only important race: when
39
42
  the initial hook runs, the exact TaskChef marker already has an entry to update.
40
- There is no 10/30-second discovery loop, scheduler, daemon, or dispatcher
41
- wakeup.
43
+ The dispatcher performs only the bounded 10/30-second identity checks. There is
44
+ no scheduler, daemon, indefinite polling, or dispatcher wakeup.
42
45
 
43
46
  Every executor receives this ownership instruction unchanged:
44
47
 
@@ -49,8 +52,8 @@ Every executor receives this ownership instruction unchanged:
49
52
  | Writer | Trigger and condition | Fields it owns |
50
53
  | --- | --- | --- |
51
54
  | Dispatcher via `record_task` | Before executor creation | New entry, `status: working`, null identity/result, server timestamps |
52
- | Dispatcher via `resolve_task` | Creation immediately returns a durable root ID | `threadId` only |
53
- | Initial `UserPromptSubmit` hook | Prompt starts with the exact TaskChef marker and the entry exists | `threadId`, initial `turnId`, `status: working`, `updatedAt`, `updatedBy: hook` |
55
+ | Dispatcher via `resolve_task` | Creation returns a durable ID, or bounded discovery verifies one exact-marker child | `threadId` only |
56
+ | Initial `UserPromptSubmit` hook | Prompt starts with the exact TaskChef marker and the verified link exists | Initial `turnId`, `status: working`, `updatedAt`, `updatedBy: hook` |
54
57
  | Follow-up `UserPromptSubmit` hook | Session ID exactly matches a recorded executor | Nothing; reads the snapshot and injects the current `turnId` for the MCP callback |
55
58
  | Executor via `report_result` | Work has a semantic outcome | `status`, bounded `summary`, result `turnId`, `updatedAt`, `updatedBy: mcp` |
56
59
  | Reporter | On explicit report request | Nothing; inferred live state is never persisted |
@@ -147,7 +150,8 @@ provide the stronger check whenever a targeted response is necessary.
147
150
  ## Permission and follow-up example
148
151
 
149
152
  1. Delegation records one `working` entry with null identity.
150
- 2. The initial hook resolves the root thread and initial turn.
153
+ 2. The dispatcher verifies and resolves the child thread; the initial hook then
154
+ records its initial turn without trusting the inherited session ID.
151
155
  3. The executor reaches a real product decision and calls `report_result` with
152
156
  `needs_input` plus “Approve deployment to production.”
153
157
  4. The user opens that executor and approves. The same hook reads the matching
@@ -168,7 +172,7 @@ live task state.
168
172
  - lifecycle event types beyond `UserPromptSubmit`
169
173
  - fork tracking or result merging
170
174
  - SQLite
171
- - polling, reconciliation schedules, daemons, and dispatcher wakeups
175
+ - indefinite polling, reconciliation schedules, daemons, and dispatcher wakeups
172
176
  - durable report watermarks
173
177
  - transcript or assistant-prose classification
174
178
  - transport-authenticated caller thread/turn identity
package/hooks/hooks.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"$PLUGIN_ROOT/hooks/taskchef-initial-prompt.js\"",
10
10
  "commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\taskchef-initial-prompt.js\"",
11
- "timeout": 10,
11
+ "timeout": 40,
12
12
  "statusMessage": "Linking TaskChef task"
13
13
  }
14
14
  ]
package/index.js CHANGED
@@ -70,4 +70,4 @@ export {
70
70
 
71
71
  export { createTaskChefMcpServer } from "./src/mcp.js";
72
72
 
73
- export { handleInitialPromptHook } from "./src/hook.js";
73
+ export { INITIAL_LINK_CHECKPOINTS_MS, handleInitialPromptHook } from "./src/hook.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "5.10.0",
3
+ "version": "5.11.1",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: taskchef-delegate
3
- description: "Dispatch actionable requests through the per-user TaskChef workspace into independently openable Codex project tasks. Use automatically for actionable work received in the canonical TaskChef dispatcher workspace. From any other project, use only when the user explicitly asks to delegate or split separate work into Codex tasks; TaskChef-related subject matter alone is not delegation intent. Record before creation, rely on the initial TaskChef hook for provisional identity, and never wait for executor completion."
3
+ description: "Dispatch actionable requests through the per-user TaskChef workspace into independently openable Codex project tasks. Use automatically for actionable work received in the canonical TaskChef dispatcher workspace. From any other project, use only when the user explicitly asks to delegate or split separate work into Codex tasks; TaskChef-related subject matter alone is not delegation intent. Record before creation, resolve provisional identity by exact marker within a strict bound, and never wait for executor completion."
4
4
  ---
5
5
 
6
6
  # TaskChef Delegate
@@ -32,11 +32,13 @@ explicitly requested benchmark artifact.
32
32
  - Keep only `AGENTS.md`, `taskchef.json`, and `tasks.jsonl` in a dispatcher
33
33
  workspace.
34
34
  - Use real Codex tasks, never collaboration or subagent tools.
35
- - Use only TaskChef's `UserPromptSubmit` hook: it resolves initial identity and
36
- provides read-only current-turn context on follow-up prompts. Never infer
35
+ - Use only TaskChef's `UserPromptSubmit` hook: after identity is separately
36
+ verified, it records the initial turn and provides read-only current-turn
37
+ context on follow-up prompts. Never infer
37
38
  lifecycle state from hooks.
38
- - Never use schedules, daemons, polling, thread-discovery retries, or background
39
- monitors.
39
+ - Never use schedules, daemons, background monitors, or indefinite polling.
40
+ The only permitted thread discovery is the bounded exact-marker resolution
41
+ after a provisional creation result.
40
42
  - Never wait for delegated work after executor creation.
41
43
  - Never collect transcripts or hidden reasoning.
42
44
 
@@ -88,13 +90,32 @@ explicitly requested benchmark artifact.
88
90
  environment on its executor host, the marked instruction, and a short title.
89
91
  8. If creation returns a durable `threadId`, call `resolve_task` immediately.
90
92
  If creation returns only `clientThreadId`, `pendingWorktreeId`, or a `local:`
91
- ID, retain it only for the created-thread directive. Return immediately; the
92
- initial TaskChef hook will atomically fill the durable root thread ID. Never
93
- list, read, wait for, or poll threads to resolve it.
93
+ ID, retain it only for the created-thread directive and run this one bounded
94
+ identity-resolution sequence:
95
+ - Check immediately, then at 10 and 30 seconds after creation. At each
96
+ checkpoint, call the native recent-task listing once.
97
+ - Restrict candidates to Codex tasks created no earlier than five seconds
98
+ before `preparedAt`, on the returned host and selected project when those
99
+ fields are available. Prefer the exact title only as an ordering hint.
100
+ - Read each remaining bounded candidate at most once per checkpoint and
101
+ accept only the single task whose structured initial
102
+ `codexDelegation.input` begins with this outcome's exact marker.
103
+ - Call `resolve_task` only for that one verified durable child ID. Never use
104
+ `session_id`, a source/parent ID, title similarity, or a provisional ID as
105
+ identity proof.
106
+ - Stop at 30 seconds. Zero matches remain unresolved; multiple exact matches
107
+ are an ambiguity and also remain unresolved. Never guess or continue
108
+ polling.
109
+
110
+ The initial hook may wait for this canonical record transition so it can
111
+ inject the verified child ID and current turn ID. Codex documents that
112
+ subagent hooks receive the parent `session_id`, so the hook must never write
113
+ that field into `threadId`.
94
114
 
95
115
  9. If executor creation fails, call `report_result` for the already-recorded
96
116
  task with `failed`, null thread/turn IDs, and a concise creation error.
97
- 10. Emit the appropriate created-thread directive and return immediately.
117
+ 10. Emit the appropriate created-thread directive and return after the bounded
118
+ identity step.
98
119
  Treat a nullable record as preserved but not yet linked. Do not read an
99
120
  executor for progress and never wait for executor work completion.
100
121
 
@@ -107,17 +128,20 @@ Keep the CLI for bootstrap, manual inspection, and recovery outside delegation.
107
128
 
108
129
  ## Later resolution
109
130
 
110
- The trusted initial hook normally resolves an unresolved task. For manual
111
- recovery, require one exact structured marker match, then call `resolve_task`
112
- once. Never edit `tasks.jsonl` directly. Resolution is an idempotent one-way
113
- transition from `threadId: null` to one unique durable root thread ID.
131
+ The trusted initial hook never resolves identity from its `session_id`. It uses
132
+ only the child ID already stored by immediate or bounded exact-marker
133
+ resolution. For manual recovery, require one exact structured marker match,
134
+ then call `resolve_task` once. Never edit `tasks.jsonl` directly. Resolution is
135
+ an idempotent one-way transition from `threadId: null` to one unique durable
136
+ child thread ID.
114
137
 
115
138
  ## Legacy benchmark compatibility
116
139
 
117
140
  The schema-v1 benchmark fixtures below describe the removed snapshot resolver
118
141
  and remain only for historical-result validation. Do not use that resolver for
119
142
  new delegation benchmarks. A future benchmark schema should measure record,
120
- creation, initial-hook resolution, and result callback without polling. The
143
+ creation, bounded exact-marker resolution, and result callback without
144
+ indefinite polling. The
121
145
  legacy fixture starts from
122
146
  `<plugin-root>/assets/e2e-benchmark-example.json`. Capture one ISO start/end
123
147
  interval for the parallel preparation/project-list operation, followed by
@@ -2,6 +2,7 @@ import {
2
2
  findCurrentTask,
3
3
  KNOWN_TASK_STATUSES,
4
4
  nextDateFilterRefreshDelay,
5
+ notificationTitle,
5
6
  reconcileNotifications,
6
7
  taskStatusLabel,
7
8
  taskWithinDateFilter,
@@ -78,7 +79,7 @@ function notificationToast(notification) {
78
79
  text.type = "button";
79
80
  text.className = "toast-content";
80
81
  const title = document.createElement("strong");
81
- title.textContent = notification.kind === "new" ? "New task" : "Task updated";
82
+ title.textContent = notificationTitle(task, notification.kind);
82
83
  const description = document.createElement("span");
83
84
  description.textContent = `${task.title} · ${taskStatusLabel(task)}`;
84
85
  text.append(title, description);
@@ -21,6 +21,10 @@ export function taskStatusLabel(task) {
21
21
  return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
22
22
  }
23
23
 
24
+ export function notificationTitle(task, kind) {
25
+ return kind === "new" ? "New task" : `Task ${taskStatusLabel(task)}`;
26
+ }
27
+
24
28
  export function taskWithinDateFilter(task, filter, now = Date.now()) {
25
29
  const windowMs = DATE_WINDOWS_MS.get(filter);
26
30
  if (windowMs === null) return true;
package/src/delegation.js CHANGED
@@ -1,8 +1,7 @@
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
+ // Bounded discovery parameters used when Codex returns only a provisional
4
+ // client ID. Candidate content still needs exact-marker verification.
6
5
  export const THREAD_RESOLUTION_CHECKPOINTS_MS = Object.freeze([10_000, 30_000]);
7
6
  export const THREAD_RESOLUTION_TIMEOUT_MS = 30_000;
8
7
  export const THREAD_RESOLUTION_RECENT_LIMIT = 50;
@@ -310,7 +309,7 @@ export async function createAndRecordDelegation(input) {
310
309
 
311
310
  return {
312
311
  status: "recorded-unresolved",
313
- reason: "awaiting-initial-hook",
312
+ reason: "awaiting-bounded-marker-resolution",
314
313
  ...prepared,
315
314
  threadId: null,
316
315
  provisional,
package/src/hook.js CHANGED
@@ -2,6 +2,10 @@ import { parseTaskChefMarker } from "./delegation.js";
2
2
  import { listTasks, startTaskFromHook } from "./workspace.js";
3
3
  import { resolveWorkspacePath } from "./workspace-path.js";
4
4
 
5
+ // The final hook check trails the dispatcher's 30-second resolver checkpoint
6
+ // so a simultaneous final match is visible before the hook fails closed.
7
+ export const INITIAL_LINK_CHECKPOINTS_MS = Object.freeze([0, 250, 1_000, 3_000, 10_000, 32_000]);
8
+
5
9
  function nonEmptyString(value) {
6
10
  return typeof value === "string" && value.trim().length > 0
7
11
  ? value.trim()
@@ -24,27 +28,59 @@ function hookContext(taskId, threadId, turnId) {
24
28
  };
25
29
  }
26
30
 
31
+ async function waitForLinkedTask(root, taskId, {
32
+ checkpoints = INITIAL_LINK_CHECKPOINTS_MS,
33
+ listTaskSnapshots = listTasks,
34
+ waitImpl = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
35
+ } = {}) {
36
+ let previousCheckpoint = 0;
37
+ for (const checkpoint of checkpoints) {
38
+ if (!Number.isFinite(checkpoint) || checkpoint < previousCheckpoint) {
39
+ throw new Error("initial-link checkpoints must be finite, non-decreasing milliseconds");
40
+ }
41
+ const delay = checkpoint - previousCheckpoint;
42
+ if (delay > 0) await waitImpl(delay);
43
+ previousCheckpoint = checkpoint;
44
+ const task = (await listTaskSnapshots(root)).find((item) => item.id === taskId);
45
+ if (task?.threadId) return task;
46
+ }
47
+ return null;
48
+ }
49
+
27
50
  export async function handleInitialPromptHook(input, {
28
51
  workspace = null,
29
52
  resolveWorkspace = () => resolveWorkspacePath().workspace,
30
53
  startTask = startTaskFromHook,
31
54
  listTaskSnapshots = listTasks,
55
+ linkCheckpoints = INITIAL_LINK_CHECKPOINTS_MS,
56
+ waitImpl = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
32
57
  } = {}) {
33
58
  if (input?.hook_event_name !== "UserPromptSubmit") return { continue: true };
34
59
  const taskId = parseTaskChefMarker(input.prompt);
35
60
  const threadId = nonEmptyString(input.session_id);
36
61
  const turnId = nonEmptyString(input.turn_id);
37
- if (threadId === null || turnId === null) {
62
+ if (turnId === null || (taskId === null && threadId === null)) {
38
63
  if (taskId === null) return { continue: true };
39
64
  return {
40
65
  continue: true,
41
- systemMessage: "TaskChef could not link this initial task because the hook payload lacked a session or turn ID.",
66
+ systemMessage: "TaskChef could not prepare this initial task because the hook payload lacked a turn ID.",
42
67
  };
43
68
  }
44
69
  if (taskId !== null) {
45
70
  const root = workspace ?? resolveWorkspace();
46
- await startTask(root, taskId, threadId, turnId);
47
- return hookContext(taskId, threadId, turnId);
71
+ const linked = await waitForLinkedTask(root, taskId, {
72
+ checkpoints: linkCheckpoints,
73
+ listTaskSnapshots,
74
+ waitImpl,
75
+ });
76
+ if (linked === null) {
77
+ return {
78
+ continue: true,
79
+ systemMessage: "TaskChef left this task unresolved because its durable child task ID could not be verified. Do not report a semantic result until the recorded identity is repaired.",
80
+ };
81
+ }
82
+ await startTask(root, taskId, linked.threadId, turnId);
83
+ return hookContext(taskId, linked.threadId, turnId);
48
84
  }
49
85
 
50
86
  try {