taskchef 5.12.0 → 6.1.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.
@@ -1,178 +1,227 @@
1
1
  # Delegation and result design
2
2
 
3
- TaskChef is deliberately smaller than a workflow engine. It records one useful
4
- snapshot per delegated task, links the executor identity once, accepts semantic
5
- results from the executor, and performs cheap freshness checks when reporting.
3
+ TaskChef stores a durable local task history while Codex owns execution. The
4
+ dispatcher records intent, the executor registers its own identity, and only
5
+ the executor reports semantic outcomes.
6
6
 
7
- ## Minimal workflow
7
+ ## Key terminology
8
+
9
+ The central identity rule is: **the executor child links itself**. The
10
+ dispatcher may create the child, but it never treats its own thread, a parent
11
+ thread, or a provisional creation handle as the executor's durable identity.
12
+
13
+ | Term | Meaning |
14
+ | --- | --- |
15
+ | TaskChef task ID | Stable UUID allocated by `prepare_dispatch`; it identifies the durable TaskChef record. |
16
+ | Exact marker | First-line correlation marker copied into the executor instruction; it binds that instruction to the pre-recorded TaskChef task. |
17
+ | Source or parent thread ID | The dispatcher/delegator context; never valid as the executor identity. |
18
+ | Executor thread ID | The child's own durable `CODEX_THREAD_ID`; the executor supplies it to `link_task`. |
19
+ | Provisional client thread ID | Temporary native creation handle; useful for creation UI, but never identity authority. |
20
+ | Turn ID | Current native turn UUIDv7 read from the exact executor thread; it proves result freshness. |
21
+ | `tasks.jsonl` | Durable append/rewrite history whose latest snapshot is shown by TaskChef. |
22
+
23
+ The diagrams below are intentionally small. Calls into **TaskChef MCP** name
24
+ the MCP function being invoked. Notes beside `tasks.jsonl` name the fields that
25
+ step populates or replaces.
26
+
27
+ ## Workflow 1: Prepare and record intent
28
+
29
+ The dispatcher first asks TaskChef to allocate routing and correlation values.
30
+ `prepare_dispatch` does not write a record. The dispatcher then embeds the exact
31
+ marker in the instruction and calls `record_task` **before** native creation, so
32
+ an executor can self-link as soon as its first turn starts.
8
33
 
9
34
  ```mermaid
10
35
  sequenceDiagram
11
- participant U as User
12
- participant D as Delegate skill
13
- participant M as TaskChef MCP
14
- participant C as Codex executor
15
- participant H as Initial hook
16
- participant W as tasks.jsonl
17
-
18
- U->>D: Delegate work
19
- par Prepare routing
20
- D->>M: prepare_dispatch
21
- and
22
- D->>C: List native projects
23
- end
24
- D->>M: record_task(threadId: null)
25
- M->>W: Append working task under lock
26
- D->>C: Create task with exact marker
27
- alt Durable ID returned
28
- D->>M: resolve_task
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)
33
- C->>H: Initial UserPromptSubmit
34
- H->>W: Wait for verified link; record initial turn
35
- end
36
- D-->>U: Return immediately
37
- C->>M: report_result(needs_input | completed | failed)
38
- M->>W: Replace latest semantic snapshot under lock
36
+ autonumber
37
+ actor U as User
38
+ participant D as Dispatcher
39
+ participant M as TaskChef MCP
40
+ participant W as tasks.jsonl
41
+
42
+ U->>D: Delegation request
43
+ D->>M: prepare_dispatch()
44
+ M-->>D: taskId, marker, preparedAt, projects
45
+ Note over D,M: Allocate identity and routing only#59; no record written
46
+ D->>M: record_task(id, project, title, marked instruction, threadId=null)
47
+ M->>W: Append schema 4 snapshot
48
+ Note right of W: id, project, title, instruction, createdAt<br>threadId=null, status=working, summary=null, turnId=null<br>updatedAt=createdAt, updatedBy=dispatcher
49
+ M-->>D: Recorded task snapshot
39
50
  ```
40
51
 
41
- Recording happens before creation. This closes the only important race: when
42
- the initial hook runs, the exact TaskChef marker already has an entry to update.
43
- The dispatcher performs only the bounded 10/30-second identity checks. There is
44
- no scheduler, daemon, indefinite polling, or dispatcher wakeup.
45
-
46
- Every executor receives this ownership instruction unchanged:
47
-
48
- > 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.
49
-
50
- ## Who writes what
51
-
52
- | Writer | Trigger and condition | Fields it owns |
53
- | --- | --- | --- |
54
- | Dispatcher via `record_task` | Before executor creation | New entry, `status: working`, null identity/result, server timestamps |
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` |
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 |
58
- | Executor via `report_result` | Work has a semantic outcome | `status`, bounded `summary`, result `turnId`, `updatedAt`, `updatedBy: mcp` |
59
- | Reporter | On explicit report request | Nothing; inferred live state is never persisted |
60
-
61
- The hook does not write needs-input, completed, or failed. Its follow-up path is
62
- read-only and exists only so the executor can report the current turn. A native permission
63
- request is live Codex state; it is not a TaskChef semantic result. The executor
64
- uses `needs_input` only when it truly requires a user decision or information.
65
-
66
- ## Task snapshot
67
-
68
- Schema version 3 retains the delegation fields and adds:
69
-
70
- - `status`: `working`, `needs_input`, `completed`, or `failed`
71
- - `summary`: null while working, otherwise a concise result capped at 2,000 characters
72
- - `turnId`: initial or latest reported turn; linked MCP results require it, and
73
- only a pre-thread creation failure may report null
74
- - `updatedAt`: server-side timestamp
75
- - `updatedBy`: `dispatcher`, `hook`, or `mcp`
76
-
77
- Schema versions 1 and 2 remain readable and normalize to nullable result fields.
78
- There is no result-event file: each callback replaces the latest snapshot on the
79
- same JSONL line.
80
- Result instructions forbid secrets, transcripts, and raw command output; the
81
- server also caps the stored summary at 2,000 characters.
82
-
83
- ## Locking and conflicts
84
-
85
- All configuration, identity, and result writes use the existing cross-process
86
- workspace lock. A writer acquires the lock, rereads and validates the complete
87
- JSONL file, changes one exact task, and publishes a complete replacement with
88
- an atomic rename. Concurrent writers therefore cannot create partial JSON,
89
- duplicate entries, or lose changes to different tasks. Sequential callbacks for
90
- the same task use last accepted write wins; normal executor turns are already
91
- sequential.
92
-
93
- SQLite is postponed because this file-level write volume is tiny and the
94
- existing lock provides the property users need. SQLite becomes worthwhile only
95
- if TaskChef later adds high-frequency event history or many continuous writers.
96
-
97
- ## Result trust
98
-
99
- The MCP server does not receive an independently authenticated caller task ID
100
- from the model transport. It validates that the supplied task exists and that
101
- the supplied durable thread ID exactly matches the recorded thread. The turn ID
102
- is stored as evidence but remains model-supplied. A trusted plugin install,
103
- local-only MCP server, bounded summary, and exact task/thread match are the
104
- current trust boundary.
105
-
106
- This is sufficient for a lightweight personal dispatcher, but not a
107
- multi-tenant authorization boundary. Transport-authenticated caller identity is
108
- postponed until Codex exposes it.
109
-
110
- ## Fresh reporting without reading every task
111
-
112
- A stored result is cached evidence, not permanent truth. Overview reports:
113
-
114
- 1. Load `tasks.jsonl` once.
115
- 2. Always consider working, needs-input, unresolved, and legacy entries.
116
- 3. Consider completed or failed entries updated in the last seven days.
117
- 4. Take one recent-thread metadata snapshot for all selected tasks. Include an
118
- older terminal task in an overview when the snapshot shows it is active or
119
- awaiting native approval.
120
- 5. A null-thread/null-turn `failed` snapshot written by MCP is a fresh creation
121
- failure and needs no live task lookup because no executor exists.
122
- 6. Treat only `updatedBy: mcp` as a semantic cache. Dispatcher- and hook-written
123
- `working` snapshots require a targeted live read; an inactive task with no
124
- callback has an unknown outcome.
125
- 7. In a broad overview, use an MCP result directly when identity is certain and
126
- the task is inactive; do not fan out detailed reads over idle terminal tasks
127
- solely because their timestamps are newer.
128
- 8. Active or awaiting-approval metadata overrides the cached result directly.
129
- For a focused task, title, or project report, read each selected inactive
130
- task at most once when matched metadata is newer than the callback by any
131
- amount. Batch targeted immediate reads, at most eight tasks per call, also
132
- for a missing callback, uncertain or contradictory state, or an explicitly
133
- fully-live request.
134
- 9. If an anomaly triggers a detailed read, compare its latest structured turn
135
- ID and native turn state with stored `turnId`. A newer turn without a
136
- callback makes the cache stale. An interrupted or cancelled callback turn
137
- cannot prove completion.
138
-
139
- An explicit task, title, or project report bypasses the seven-day overview
140
- filter. Old terminal tasks skipped from an overview are counted so the user
141
- knows history was intentionally omitted.
142
-
143
- The cheap operation is the single list/metadata snapshot, not one read per
144
- historical task. It is sufficient to expose active and native-approval state for
145
- many recent tasks at once. It does not prove completion; semantic outcomes come
146
- from MCP callbacks. Detailed thread reads are the exceptional fallback.
147
- Timestamps are a pragmatic anomaly filter; turn IDs and native turn state
148
- provide the stronger check whenever a targeted response is necessary.
149
-
150
- ## Permission and follow-up example
151
-
152
- 1. Delegation records one `working` entry with null identity.
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.
155
- 3. The executor reaches a real product decision and calls `report_result` with
156
- `needs_input` plus “Approve deployment to production.”
157
- 4. The user opens that executor and approves. The same hook reads the matching
158
- task and injects the new turn ID without changing the stored snapshot. Until
159
- the final callback, a report sees newer/active live metadata and labels the
160
- cached needs-input result stale.
161
- 5. The executor finishes and calls `report_result` with `completed`, the new
162
- turn ID, and a concise outcome. The same JSONL line now contains the completed
163
- snapshot.
164
-
165
- If step 3 were merely Codex asking for filesystem or command approval, no MCP
166
- callback would be written. Reporting would show “awaiting native approval” from
167
- live task state.
168
-
169
- ## Explicitly postponed
170
-
171
- - append-only result or transition events
172
- - lifecycle event types beyond `UserPromptSubmit`
173
- - fork tracking or result merging
174
- - SQLite
175
- - indefinite polling, reconciliation schedules, daemons, and dispatcher wakeups
176
- - durable report watermarks
177
- - transcript or assistant-prose classification
178
- - transport-authenticated caller thread/turn identity
52
+ ## Workflow 2: Create and self-link the executor
53
+
54
+ After recording, the dispatcher creates the native Codex task and returns
55
+ immediately. A durable or provisional ID returned to the dispatcher is not
56
+ trusted as executor identity. The child reads its own `CODEX_THREAD_ID` and
57
+ uses `link_task` as its first TaskChef action.
58
+
59
+ The exact marker correlates the child instruction with the pre-created record.
60
+ `link_task` atomically permits one `null`-to-durable transition, rejects a
61
+ malformed or reused ID, and makes the dashboard deep link target the child—not
62
+ the source, parent, or dispatcher thread.
63
+
64
+ ```mermaid
65
+ sequenceDiagram
66
+ autonumber
67
+ actor U as User
68
+ participant D as Dispatcher
69
+ participant C as Codex native tasks
70
+ participant E as Executor child
71
+ participant M as TaskChef MCP
72
+ participant W as tasks.jsonl
73
+ participant V as Dashboard
74
+
75
+ D->>C: Create task with marked instruction
76
+ C-->>D: Durable threadId or provisional clientThreadId
77
+ Note over D,C: Creation result is not executor identity authority
78
+ D-->>U: Created-task directive#59; dispatcher returns immediately
79
+ C->>E: Start initial executor turn
80
+ E->>E: Read own CODEX_THREAD_ID
81
+ E->>M: link_task(taskId, child threadId)
82
+ M->>W: Atomic identity registration
83
+ Note right of W: Set threadId=child UUIDv7<br>Set updatedAt and updatedBy=mcp<br>Keep status=working, summary=null, turnId=null
84
+ M-->>E: Self-linked task snapshot
85
+ W-->>V: Filesystem watcher refresh
86
+ Note right of V: Deep link targets the exact executor child
87
+ ```
88
+
89
+ ## Workflow 3: Report the current turn
90
+
91
+ Before ending with a semantic outcome, the executor reads its exact native
92
+ thread and obtains that turn's current UUIDv7. It then calls `report_result`
93
+ with its matching self-linked thread ID. The status is `completed`, `failed`,
94
+ or `needs_input`; the summary is concise and bounded.
95
+
96
+ Changed results must use a strictly newer turn ID. An identical same-turn retry
97
+ is idempotent, but reusing an old turn ID for a changed result cannot establish
98
+ freshness.
99
+
100
+ ```mermaid
101
+ sequenceDiagram
102
+ autonumber
103
+ participant E as Executor child
104
+ participant C as Codex native tasks
105
+ participant M as TaskChef MCP
106
+ participant W as tasks.jsonl
107
+ participant V as Dashboard
108
+
109
+ E->>C: Exact read of this executor thread
110
+ C-->>E: Current turnId UUIDv7
111
+ E->>M: report_result(taskId, threadId, turnId, status, summary)
112
+ M->>W: Store semantic result
113
+ Note right of W: Replace status, summary, turnId<br>Set updatedAt and updatedBy=mcp
114
+ M-->>E: Updated task snapshot
115
+ W-->>V: Filesystem watcher refresh
116
+ ```
117
+
118
+ ## Workflow 4: Resume after `needs_input`
119
+
120
+ `needs_input` is a semantic pause, not a native approval prompt. After the user
121
+ responds or resumes the task, the executor reads the exact task again. The new
122
+ turn ID, rather than the initial one, accompanies the updated result.
123
+
124
+ ```mermaid
125
+ sequenceDiagram
126
+ autonumber
127
+ actor U as User
128
+ participant E as Executor child
129
+ participant C as Codex native tasks
130
+ participant M as TaskChef MCP
131
+ participant W as tasks.jsonl
132
+ participant V as Dashboard
133
+
134
+ E-->>U: Request decision or information
135
+ U->>E: Follow up or resume
136
+ E->>C: Exact native read after follow-up
137
+ C-->>E: Newer current turnId UUIDv7
138
+ E->>M: report_result(taskId, threadId, newer turnId, completed, summary)
139
+ M->>W: Replace latest semantic result
140
+ Note right of W: status=completed, new summary, newer turnId<br>updatedAt refreshed, updatedBy=mcp
141
+ M-->>E: Completed task snapshot
142
+ W-->>V: Filesystem watcher refresh
143
+ ```
144
+
145
+ ## Workflow 5: Creation and linking failures
146
+
147
+ Because `record_task` happens first, native creation failure remains visible:
148
+ the dispatcher reports one terminal failure while both identity fields stay
149
+ null. The dispatcher makes exactly one native creation call and never retries
150
+ it. TaskChef never guesses an executor identity from recent tasks, titles,
151
+ transcripts, or dashboard activity.
152
+
153
+ For linking, outcome depends on where interruption occurs. Before commit, an
154
+ eligible record remains link-pending. If the atomic write commits but its reply
155
+ is lost, the record is already linked and an identical retry returns that
156
+ snapshot. Inspect rejections before retrying; identity conflicts and terminal
157
+ records must not be blindly retried.
158
+
159
+ ```mermaid
160
+ sequenceDiagram
161
+ autonumber
162
+ actor U as User
163
+ participant D as Dispatcher
164
+ participant C as Codex native tasks
165
+ participant E as Executor child
166
+ participant M as TaskChef MCP
167
+ participant W as tasks.jsonl
168
+
169
+ alt Native creation fails after record_task
170
+ D->>C: Create task with marked instruction
171
+ C--xD: Creation error
172
+ D->>M: report_result(taskId, null, null, failed, bounded summary)
173
+ M->>W: Store terminal creation failure
174
+ Note right of W: Keep threadId=null and turnId=null<br>Set status=failed, summary, updatedAt, updatedBy=mcp
175
+ M-->>D: Failed task snapshot
176
+ D-->>U: Creation failure with preserved TaskChef taskId
177
+ else Initial link stops before commit
178
+ E--xM: link_task(taskId, child threadId)
179
+ Note right of W: Remains threadId=null, status=working<br>Eligible link-pending record may retry later
180
+ else Link commits but response is lost
181
+ E->>M: link_task(taskId, child threadId)
182
+ M->>W: Commit child threadId
183
+ M--xE: Response lost
184
+ E->>M: Identical link_task retry
185
+ M-->>E: Return existing linked snapshot
186
+ end
187
+ ```
188
+
189
+ ## MCP calls and field transitions
190
+
191
+ | Step | Caller | Operation | Key input | Task record effect |
192
+ | --- | --- | --- | --- | --- |
193
+ | 1 | Dispatcher | `prepare_dispatch` | No task identity supplied | Returns `taskId`, exact `marker`, `preparedAt`, and configured `projects`; does not write `tasks.jsonl`. |
194
+ | 2 | Dispatcher | `record_task` | `id`, `project`, `title`, marked `instruction`, `threadId: null` | Appends schema 4 with `createdAt`; sets `status: working`, `summary: null`, `turnId: null`, `updatedAt: createdAt`, `updatedBy: dispatcher`. |
195
+ | 3 | Dispatcher | Native Codex task creation—not MCP | Target project plus marked instruction | Does not change the TaskChef record. A returned durable or provisional ID is not identity authority. |
196
+ | 4 | Executor | `link_task` | Marked `taskId` plus its own `CODEX_THREAD_ID` | Atomically changes `threadId` from `null` to the canonical child UUIDv7; refreshes `updatedAt` and sets `updatedBy: mcp`. |
197
+ | 5 | Executor | `report_result` | Exact `taskId`, self-linked `threadId`, current `turnId`, semantic `status`, concise `summary` | Replaces the latest `status`, `summary`, and `turnId`; refreshes `updatedAt` and sets `updatedBy: mcp`. Changed follow-up results require a newer UUIDv7 `turnId`. |
198
+ | Failure | Dispatcher | `report_result` after native creation error | `taskId`, `threadId: null`, `turnId: null`, `status: failed`, bounded `summary` | Preserves the pre-created record and null identity while storing a terminal creation failure. |
199
+
200
+ ## Design boundaries
201
+
202
+ There is no task listing, candidate read, marker search, wait, polling loop,
203
+ native creation retry, transcript read, or hook in the dispatch path. The
204
+ filesystem watcher notices atomic TaskChef writes and refreshes only the
205
+ dashboard; reports read current state on demand.
206
+
207
+ Custom MCP does not authenticate the calling Codex task. The executor thread ID
208
+ is therefore a cooperative assertion inside TaskChef's local single-user trust
209
+ boundary. The design prevents accidental parent/child confusion but does not
210
+ claim resistance to a deliberately forged local MCP call.
211
+
212
+ Historical `updatedBy: hook` values remain readable, but new installations
213
+ contain no hook and new writes use `dispatcher` or `mcp`.
214
+
215
+ ## Legacy recovery
216
+
217
+ `taskchef task resolve` is retained only for unresolved schema 1-3 records.
218
+ Operators must establish one exact marker match and one unique durable child
219
+ ID. Schema 4 self-linking records reject manual resolution. History is read
220
+ compatibly and is not eagerly rewritten.
221
+
222
+ ## Dashboard and reports
223
+
224
+ The dashboard deep link uses only the stored self-linked child ID. File watcher
225
+ events surface linking and results without user interaction. Reporting may
226
+ compare current native metadata with cached semantic results, but it never
227
+ writes inferred lifecycle state.
package/index.js CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  filterTasks,
10
10
  importProjects,
11
11
  initializeWorkspace,
12
+ linkTask,
12
13
  listProjects,
13
14
  prepareDispatch,
14
15
  readConfig,
@@ -17,7 +18,6 @@ export {
17
18
  recordTask,
18
19
  reportTaskResult,
19
20
  resolveTask,
20
- startTaskFromHook,
21
21
  requireSafeId,
22
22
  removeProject,
23
23
  validateConfig,
@@ -31,20 +31,14 @@ export {
31
31
 
32
32
  export {
33
33
  EXECUTOR_OWNERSHIP_PARAGRAPH,
34
+ EXECUTOR_LINK_PARAGRAPH,
34
35
  EXECUTOR_RESULT_PARAGRAPH,
35
- THREAD_RESOLUTION_CHECKPOINTS_MS,
36
- THREAD_RESOLUTION_CLOCK_SKEW_MS,
37
- THREAD_RESOLUTION_RECENT_LIMIT,
38
- THREAD_RESOLUTION_TIMEOUT_MS,
39
36
  createAndRecordDelegation,
40
- filterThreadCandidates,
41
- hasExactTaskChefMarker,
42
37
  isProvisionalThreadId,
43
- listThreadEntries,
38
+ normalizeCodexThreadId,
44
39
  normalizeDurableThreadId,
45
40
  parseTaskChefMarker,
46
41
  prepareDelegation,
47
- structuredDelegatedInputs,
48
42
  taskChefMarker,
49
43
  } from "./src/delegation.js";
50
44
 
@@ -69,5 +63,3 @@ export {
69
63
  } from "./src/dashboard.js";
70
64
 
71
65
  export { createTaskChefMcpServer } from "./src/mcp.js";
72
-
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.12.0",
3
+ "version": "6.1.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -24,7 +24,6 @@
24
24
  "BACKLOG.md",
25
25
  "bin",
26
26
  "docs/delegation-design.md",
27
- "hooks",
28
27
  "index.js",
29
28
  "mcp",
30
29
  "SPEC.md",
@@ -40,6 +39,7 @@
40
39
  },
41
40
  "devDependencies": {
42
41
  "conventional-changelog-conventionalcommits": "^10.3.0",
42
+ "mermaid": "^11.17.0",
43
43
  "yaml": "^2.9.0"
44
44
  },
45
45
  "scripts": {