taskchef 7.0.0 → 7.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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +15 -10
- package/docs/firstmate-taskchef-comparison.md +7 -5
- package/docs/spec.md +42 -23
- package/docs/workflows.md +34 -21
- package/index.js +2 -0
- package/package.json +1 -1
- package/skills/taskchef-delegate/SKILL.md +13 -8
- package/skills/taskchef-report/SKILL.md +14 -14
- package/src/cli.js +7 -3
- package/src/dashboard/app.js +8 -4
- package/src/dashboard/state.js +1 -0
- package/src/dashboard.js +2 -0
- package/src/delegation.js +3 -2
- package/src/mcp.js +38 -4
- package/src/workspace.js +289 -38
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ work, approvals, and follow-ups happen; TaskChef keeps the latest compact
|
|
|
7
7
|
snapshot for navigation and reporting.
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
request -> recorded TaskChef task -> Codex executor ->
|
|
10
|
+
request -> recorded TaskChef task -> Codex executor -> current state + last result
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Which document should I read?
|
|
@@ -48,7 +48,7 @@ The canonical workspace is `~/.agents/taskchef`. TaskChef owns only:
|
|
|
48
48
|
```text
|
|
49
49
|
AGENTS.md managed dispatcher instructions plus user additions
|
|
50
50
|
taskchef.json schema-2 configured projects and routing metadata
|
|
51
|
-
tasks.jsonl one schema-4 snapshot per task
|
|
51
|
+
tasks.jsonl one schema-4/5 snapshot per task (new writes use schema 5)
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
List or change routing targets conversationally:
|
|
@@ -92,17 +92,22 @@ outcomes may become separate executors; dependent work should stay together.
|
|
|
92
92
|
|
|
93
93
|
## Work with and report executors
|
|
94
94
|
|
|
95
|
-
Open an executor as an ordinary Codex task. Each executor reports
|
|
96
|
-
semantic outcome:
|
|
95
|
+
Open an executor as an ordinary Codex task. Each executor reports `working`
|
|
96
|
+
when a turn starts and one semantic outcome before that same turn ends:
|
|
97
97
|
|
|
98
|
-
- `completed`: the requested outcome is complete
|
|
99
|
-
- `needs_input`: a real user decision or missing fact blocks progress
|
|
100
|
-
- `failed`:
|
|
98
|
+
- `completed`: the requested outcome is complete;
|
|
99
|
+
- `needs_input`: a real user decision or missing fact blocks progress;
|
|
100
|
+
- `failed`: the executor or creation attempt ended unsuccessfully.
|
|
101
101
|
|
|
102
|
-
A native approval prompt is live Codex state, not `needs_input`.
|
|
103
|
-
stores
|
|
102
|
+
A native approval prompt is live Codex state, not `needs_input`.
|
|
103
|
+
TaskChef stores the current reported execution state and separately preserves
|
|
104
|
+
the last concise semantic result. A follow-up therefore appears as `working`
|
|
105
|
+
immediately without erasing the previous outcome. TaskChef does not store the
|
|
106
|
+
transcript or a lifecycle event log.
|
|
104
107
|
|
|
105
|
-
|
|
108
|
+
## View and report tasks
|
|
109
|
+
|
|
110
|
+
Ask the dispatcher for an on-demand report:
|
|
106
111
|
|
|
107
112
|
```text
|
|
108
113
|
Report on the work TaskChef has dispatched.
|
|
@@ -161,9 +161,11 @@ backends have different verification or experimental status.
|
|
|
161
161
|
### TaskChef
|
|
162
162
|
|
|
163
163
|
**Fact:** Persisted statuses are `working`, `needs_input`, `completed`, and
|
|
164
|
-
`failed`. Executors report
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
`failed`. Executors report
|
|
165
|
+
`working` at turn start and a semantic state before ending. Schema 5 preserves
|
|
166
|
+
the last concise semantic result separately while a newer turn is working.
|
|
167
|
+
`needs_input` is reserved for a real semantic decision, not a native approval
|
|
168
|
+
prompt.
|
|
167
169
|
|
|
168
170
|
**Fact:** The report skill treats cached results as evidence, takes one recent
|
|
169
171
|
Codex metadata snapshot, and performs targeted exact-thread reads only for
|
|
@@ -189,7 +191,7 @@ can be substituted for the other without changing product responsibility.
|
|
|
189
191
|
**Fact:** The loopback dashboard watches `tasks.jsonl`, streams stable validated
|
|
190
192
|
snapshots, filters and orders tasks, shows notifications and details, and opens
|
|
191
193
|
a task directly in Codex when its stored thread ID has a supported UUID shape.
|
|
192
|
-
This navigation check does not prove schema 4 self-link provenance. The
|
|
194
|
+
This navigation check does not prove schema 4 or 5 self-link provenance. The
|
|
193
195
|
dashboard does not refresh native task state, submit replies, or show
|
|
194
196
|
transcripts or token usage.
|
|
195
197
|
|
|
@@ -214,7 +216,7 @@ harness adapter.
|
|
|
214
216
|
| Codex | Built around native Codex projects, tasks, thread reads, and desktop deep links. | Codex is one verified harness; desktop tasks are not its worker runtime. |
|
|
215
217
|
| GitHub | Configured repository URLs aid routing; delivery remains the executor project's concern. | GitHub CLI, PR state, and configured delivery modes are part of supervised shipping workflows. |
|
|
216
218
|
| Skills | Three plugin skills with narrow bootstrap, delegate, and report responsibilities. | Internal firstmate-only skills plus standalone public skills; AGENTS.md routes conditional procedures. |
|
|
217
|
-
| MCP | Four
|
|
219
|
+
| MCP | Four primary local TaskChef tools plus one deprecated compatibility alias own deterministic identity and state writes. | FirstMate describes itself as an agent distribution rather than an MCP product; deterministic behavior lives largely in scripts. |
|
|
218
220
|
| Runtime extensions | Extend plugin skills/MCP/CLI/dashboard or compose native Codex capabilities. | Add or verify harness adapters, session backends, dispatch profiles, operational scripts, and optional integrations. |
|
|
219
221
|
|
|
220
222
|
## Trust and security boundaries
|
package/docs/spec.md
CHANGED
|
@@ -22,7 +22,8 @@ is dated research, not contract.
|
|
|
22
22
|
| **Record-before-create** | Persisting a link-pending task before asking Codex to create its executor. |
|
|
23
23
|
| **Self-linking** | The executor's one-way registration of its own canonical Codex UUIDv7 from `CODEX_THREAD_ID`. |
|
|
24
24
|
| **Link-pending** | A working task whose `threadId` is null and `updatedBy` is `dispatcher`. |
|
|
25
|
-
| **
|
|
25
|
+
| **Current execution state** | The latest reported executor turn and its `working`, `needs_input`, `completed`, or `failed` status. |
|
|
26
|
+
| **Last semantic result** | The most recent `completed`, `needs_input`, or `failed` outcome, preserved separately while a newer turn is working. |
|
|
26
27
|
| **Current turn ID** | The canonical Codex UUIDv7 returned by an exact native read of the linked executor for the turn being reported. |
|
|
27
28
|
| **Dashboard** | The loopback, read-only UI derived from validated workspace snapshots and bounded native actions. |
|
|
28
29
|
| **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-delegate`, or `taskchef-report`. |
|
|
@@ -35,7 +36,8 @@ is dated research, not contract.
|
|
|
35
36
|
It MUST return after creation and MUST NOT poll, supervise, or infer identity.
|
|
36
37
|
- `taskchef-report` MUST own on-demand reporting. It MUST NOT poll or persist
|
|
37
38
|
inferred state.
|
|
38
|
-
- The MCP server MUST expose
|
|
39
|
+
- The MCP server MUST expose four primary lifecycle tools plus the deprecated
|
|
40
|
+
`report_result` compatibility alias specified below.
|
|
39
41
|
- The CLI MAY administer and inspect the workspace, but MUST NOT provide a
|
|
40
42
|
second agent lifecycle protocol.
|
|
41
43
|
- The dashboard MUST be read-only with respect to dispatcher files.
|
|
@@ -62,9 +64,11 @@ Names and paths MUST be unique. Git projects MUST be exact Git roots.
|
|
|
62
64
|
Repository URLs MUST canonicalize to `https://github.com/<owner>/<repository>`
|
|
63
65
|
and be case-insensitively deduplicated.
|
|
64
66
|
|
|
65
|
-
`tasks.jsonl` MUST contain zero or more newline-terminated schema-4
|
|
66
|
-
one per line.
|
|
67
|
-
|
|
67
|
+
`tasks.jsonl` MUST contain zero or more newline-terminated schema-4 or schema-5
|
|
68
|
+
records, one per line. Schema 4 is read compatibility for the previously
|
|
69
|
+
released format; every new record and state mutation MUST write schema 5.
|
|
70
|
+
Other schemas or unsupported fields MUST be rejected without conversion.
|
|
71
|
+
Reads and writes MUST reject symlinked managed files. Mutations
|
|
68
72
|
MUST hold the shared workspace lock and replace state atomically; read-only
|
|
69
73
|
operations MUST NOT require write permission.
|
|
70
74
|
|
|
@@ -74,7 +78,7 @@ Every record MUST contain exactly these fields:
|
|
|
74
78
|
|
|
75
79
|
| Field | Contract |
|
|
76
80
|
| --- | --- |
|
|
77
|
-
| `schemaVersion` | Integer `4
|
|
81
|
+
| `schemaVersion` | Integer `5`; schema-4 records remain readable until their next mutation. |
|
|
78
82
|
| `id` | Unique safe TaskChef ID; delegation uses a lowercase full UUID. |
|
|
79
83
|
| `project` | Immutable configured-project snapshot. |
|
|
80
84
|
| `title` | Non-empty display title. |
|
|
@@ -82,10 +86,11 @@ Every record MUST contain exactly these fields:
|
|
|
82
86
|
| `threadId` | Null while link-pending; after self-link, canonical Codex UUIDv7. Low-level current-schema direct records may hold another durable non-provisional ID but are outside the MCP delegation journey. |
|
|
83
87
|
| `createdAt` | ISO 8601 creation timestamp. |
|
|
84
88
|
| `status` | `working`, `needs_input`, `completed`, or `failed`. |
|
|
85
|
-
| `summary` | Null while working; non-empty
|
|
86
|
-
| `turnId` | Null before
|
|
87
|
-
| `updatedAt` | ISO 8601 timestamp not earlier than `createdAt
|
|
89
|
+
| `summary` | Null while working; otherwise the current semantic state's non-empty summary of at most 2,000 characters. |
|
|
90
|
+
| `turnId` | Null before turn reporting; otherwise the current reported turn. Linked MCP journeys use a canonical Codex UUIDv7. |
|
|
91
|
+
| `updatedAt` | ISO 8601 timestamp not earlier than `createdAt` or the prior `updatedAt`; clock rollback cannot backdate a transition. |
|
|
88
92
|
| `updatedBy` | `dispatcher` or `mcp`. |
|
|
93
|
+
| `lastResult` | Null before a semantic result; otherwise `{status, summary, turnId, updatedAt}` preserving the latest semantic result. |
|
|
89
94
|
|
|
90
95
|
Task IDs and non-null thread identities MUST be unique. The immutable intent
|
|
91
96
|
fields MUST NOT change after recording.
|
|
@@ -100,12 +105,15 @@ fields MUST NOT change after recording.
|
|
|
100
105
|
5. It MUST create exactly one native Codex executor and return immediately.
|
|
101
106
|
6. The executor MUST read its own `CODEX_THREAD_ID` and call `link_task`
|
|
102
107
|
before substantive work. It MUST NOT use parent/session identity or guess.
|
|
103
|
-
7.
|
|
104
|
-
|
|
105
|
-
8.
|
|
108
|
+
7. After initial linking, the executor MUST exactly read its linked task and
|
|
109
|
+
call `report_state` with that turn ID, `working`, and no summary before work.
|
|
110
|
+
8. Before ending, it MUST call `report_state` for the same working turn with a
|
|
111
|
+
semantic status and concise summary.
|
|
112
|
+
9. A follow-up MUST report `working` with its new current turn ID before work.
|
|
113
|
+
It MUST NOT reuse a prior turn.
|
|
106
114
|
|
|
107
115
|
If native creation fails after recording, the dispatcher MUST call
|
|
108
|
-
`
|
|
116
|
+
`report_state` with `failed`, null thread/turn IDs, and a bounded summary.
|
|
109
117
|
A link failure MUST remain visible and retryable; the executor MUST report it
|
|
110
118
|
visibly and MUST NOT continue substantive work.
|
|
111
119
|
|
|
@@ -163,7 +171,7 @@ new preparation values, though it writes no state.
|
|
|
163
171
|
|
|
164
172
|
**Structured output:** `{ task: Task }`.
|
|
165
173
|
|
|
166
|
-
The returned task has schema
|
|
174
|
+
The returned task has schema 5, `working`, null summary/turn/thread/lastResult,
|
|
167
175
|
`updatedBy: dispatcher`, and equal creation/update timestamps. Duplicate IDs,
|
|
168
176
|
unknown projects, malformed markers, and invalid input fail. Repeating a
|
|
169
177
|
successful call is not idempotent; it fails as a duplicate.
|
|
@@ -189,10 +197,10 @@ marker, or ineligible state fails.
|
|
|
189
197
|
**Annotations:** `readOnlyHint: false`, `destructiveHint: false`,
|
|
190
198
|
`openWorldHint: false`.
|
|
191
199
|
|
|
192
|
-
### `
|
|
200
|
+
### `report_state`
|
|
193
201
|
|
|
194
202
|
**Caller:** executor, or dispatcher only for native creation failure.
|
|
195
|
-
**Mutation:** replaces the
|
|
203
|
+
**Mutation:** replaces the current state atomically and preserves `lastResult`.
|
|
196
204
|
|
|
197
205
|
**Input:**
|
|
198
206
|
|
|
@@ -201,20 +209,31 @@ marker, or ineligible state fails.
|
|
|
201
209
|
| `taskId` | Non-empty string. |
|
|
202
210
|
| `threadId` | Matching non-empty ID for a linked task; null only for creation failure. |
|
|
203
211
|
| `turnId` | Current canonical Codex UUIDv7 for a linked MCP journey; null only for creation failure. Maximum 256 characters at the MCP boundary. |
|
|
204
|
-
| `status` | `needs_input`, `completed`, or `failed`. |
|
|
205
|
-
| `summary` |
|
|
212
|
+
| `status` | `working`, `needs_input`, `completed`, or `failed`. |
|
|
213
|
+
| `summary` | Omitted or null for `working`; required non-empty string of at most 2,000 characters otherwise. |
|
|
206
214
|
|
|
207
215
|
**Structured output:** `{ task: Task }`.
|
|
208
216
|
|
|
209
|
-
For a linked self-linking journey,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
A null-identity record accepts only
|
|
213
|
-
|
|
217
|
+
For a linked self-linking journey, `working` MUST identify a turn newer than
|
|
218
|
+
the current turn and last semantic result. A semantic state MUST match the
|
|
219
|
+
current working turn. Repeating an identical state is idempotent; conflicting
|
|
220
|
+
or older state fails. A null-identity record accepts only a fresh executor
|
|
221
|
+
creation `failed` state with both IDs null. Success sets the current state and
|
|
222
|
+
preserves the semantic state in `lastResult`; starting newer work does not erase
|
|
223
|
+
that result.
|
|
214
224
|
|
|
215
225
|
**Annotations:** `readOnlyHint: false`, `destructiveHint: true`,
|
|
216
226
|
`openWorldHint: false`.
|
|
217
227
|
|
|
228
|
+
### `report_result` (deprecated)
|
|
229
|
+
|
|
230
|
+
`report_result` retains the prior semantic-only input shape and statuses as a
|
|
231
|
+
temporary compatibility alias. It implicitly accepts a fresh supplied turn and
|
|
232
|
+
stores its semantic result, including for supported schema-4 records and
|
|
233
|
+
low-level opaque direct records. It does not accept `working`. New executor
|
|
234
|
+
instructions MUST use `report_state`. Successful mutation upgrades schema 4 to
|
|
235
|
+
schema 5; unsupported schemas remain rejected.
|
|
236
|
+
|
|
218
237
|
## Reporting and dashboard
|
|
219
238
|
|
|
220
239
|
A semantic result is cached evidence, not permanent live truth. Reports SHOULD
|
package/docs/workflows.md
CHANGED
|
@@ -13,7 +13,7 @@ research.
|
|
|
13
13
|
| `skills/taskchef-delegate/SKILL.md` | Split, route, record-before-create, create, return. |
|
|
14
14
|
| `skills/taskchef-bootstrap/SKILL.md` | Initialize current workspace and configure projects. |
|
|
15
15
|
| `skills/taskchef-report/SKILL.md` | Select cached tasks and perform bounded live checks. |
|
|
16
|
-
| `src/mcp.js` | Four
|
|
16
|
+
| `src/mcp.js` | Four primary lifecycle tools, one deprecated alias, and MCP annotations. |
|
|
17
17
|
| `src/delegation.js` | UUID marker, executor contract paragraphs, and creation-failure handling. |
|
|
18
18
|
| `src/workspace.js` | Current schemas, validation, locking, atomic JSONL writes, linking, and result freshness. |
|
|
19
19
|
| `src/cli.js` | Administration, inspection, diagnostics, and dashboard startup. |
|
|
@@ -49,7 +49,7 @@ sequenceDiagram
|
|
|
49
49
|
D->>D: Choose one configured and native project
|
|
50
50
|
D->>M: record_task(id, project, title, instruction, null)
|
|
51
51
|
M->>W: recordTask()
|
|
52
|
-
W->>W: Lock, validate, append schema-
|
|
52
|
+
W->>W: Lock, validate, append schema-5 snapshot
|
|
53
53
|
W-->>M: working link-pending task
|
|
54
54
|
M-->>D: task
|
|
55
55
|
D->>C: Create executor with marked instruction
|
|
@@ -67,10 +67,11 @@ Record-before-create makes native creation failure observable. Executor
|
|
|
67
67
|
self-linking removes dispatcher-side polling, task search, title matching, and
|
|
68
68
|
parent/child identity inference.
|
|
69
69
|
|
|
70
|
-
##
|
|
70
|
+
## State reporting
|
|
71
71
|
|
|
72
72
|
The executor obtains the turn identity from an exact native read of its own
|
|
73
|
-
linked task. `
|
|
73
|
+
linked task. `report_state` records live turn state while preserving the last
|
|
74
|
+
semantic result separately.
|
|
74
75
|
|
|
75
76
|
```mermaid
|
|
76
77
|
sequenceDiagram
|
|
@@ -79,14 +80,17 @@ sequenceDiagram
|
|
|
79
80
|
participant C as Native Codex task API
|
|
80
81
|
participant M as TaskChef MCP
|
|
81
82
|
participant W as workspace.js
|
|
82
|
-
E->>E: Finish or reach semantic decision
|
|
83
83
|
E->>C: Exact read of linked executor
|
|
84
84
|
C-->>E: Current turn ID
|
|
85
|
-
E->>M:
|
|
86
|
-
M->>W:
|
|
85
|
+
E->>M: report_state(..., working, null)
|
|
86
|
+
M->>W: reportTaskState()
|
|
87
|
+
W->>W: Store current turn and preserve lastResult
|
|
88
|
+
E->>E: Work, finish, or reach semantic decision
|
|
89
|
+
E->>M: report_state(..., semantic status, summary)
|
|
90
|
+
M->>W: reportTaskState()
|
|
87
91
|
W->>W: Lock and validate identity and freshness
|
|
88
|
-
alt
|
|
89
|
-
W->>W:
|
|
92
|
+
alt Same current working turn
|
|
93
|
+
W->>W: Store semantic state and lastResult
|
|
90
94
|
W-->>M: Updated task
|
|
91
95
|
M-->>E: Recorded result
|
|
92
96
|
else Same turn and same result
|
|
@@ -114,18 +118,26 @@ sequenceDiagram
|
|
|
114
118
|
participant C as Native Codex task API
|
|
115
119
|
participant M as TaskChef MCP
|
|
116
120
|
participant W as workspace.js
|
|
117
|
-
E->>M:
|
|
118
|
-
M->>W:
|
|
119
|
-
W-->>
|
|
121
|
+
E->>M: report_state(..., turnA, needs_input, summaryA)
|
|
122
|
+
M->>W: reportTaskState()
|
|
123
|
+
W-->>M: needs_input snapshot
|
|
124
|
+
M-->>E: needs_input snapshot
|
|
120
125
|
U->>E: Provide decision
|
|
121
126
|
E->>C: Read exact executor after follow-up
|
|
122
127
|
C-->>E: turnB
|
|
123
|
-
E->>M:
|
|
124
|
-
M->>W:
|
|
125
|
-
W
|
|
126
|
-
|
|
128
|
+
E->>M: report_state(..., turnB, working, null)
|
|
129
|
+
M->>W: reportTaskState()
|
|
130
|
+
W->>W: Require turnB greater and preserve result A
|
|
131
|
+
W-->>M: working snapshot plus lastResult A
|
|
132
|
+
M-->>E: working snapshot plus lastResult A
|
|
133
|
+
E->>M: report_state(..., turnB, completed, summaryB)
|
|
134
|
+
M->>W: reportTaskState()
|
|
135
|
+
W-->>M: completed snapshot plus result B
|
|
136
|
+
M-->>E: completed snapshot plus result B
|
|
137
|
+
E->>M: report_state(..., turnA, completed, staleSummary)
|
|
127
138
|
M->>W: Validate freshness
|
|
128
|
-
W-->>
|
|
139
|
+
W-->>M: Error: turn is not newer
|
|
140
|
+
M-->>E: Visible tool error
|
|
129
141
|
```
|
|
130
142
|
|
|
131
143
|
The executor contract therefore requires a new exact read on every follow-up;
|
|
@@ -174,7 +186,7 @@ sequenceDiagram
|
|
|
174
186
|
W-->>D: Recorded task
|
|
175
187
|
D->>C: Create executor
|
|
176
188
|
C--xD: Creation error
|
|
177
|
-
D->>M:
|
|
189
|
+
D->>M: report_state(taskId, null, null, failed, boundedSummary)
|
|
178
190
|
M->>W: Lock and store creation failure
|
|
179
191
|
W-->>D: Failed task with null IDs
|
|
180
192
|
D-->>D: Preserve original creation error and task ID
|
|
@@ -198,7 +210,7 @@ sequenceDiagram
|
|
|
198
210
|
participant D as Dashboard monitor
|
|
199
211
|
participant B as Browser client
|
|
200
212
|
participant C as Native Codex
|
|
201
|
-
M->>W: link_task or
|
|
213
|
+
M->>W: link_task or report_state
|
|
202
214
|
W->>W: Acquire shared lock
|
|
203
215
|
W->>F: Atomic replacement
|
|
204
216
|
W-->>M: Updated task
|
|
@@ -232,5 +244,6 @@ summary is cryptographically authenticated; this is a local single-user trust
|
|
|
232
244
|
model. Managed files, instructions, project snapshots, MCP inputs, and dashboard
|
|
233
245
|
requests are validated at every action boundary.
|
|
234
246
|
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
Configuration schema 2 and task schemas 4 and 5 are accepted. Schema 4 is
|
|
248
|
+
read-only compatibility until a lifecycle mutation upgrades that record to
|
|
249
|
+
schema 5. Other schemas are rejected without rewrite.
|
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export {
|
|
|
15
15
|
listTasks,
|
|
16
16
|
readTask,
|
|
17
17
|
recordTask,
|
|
18
|
+
reportTaskState,
|
|
18
19
|
reportTaskResult,
|
|
19
20
|
requireSafeId,
|
|
20
21
|
removeProject,
|
|
@@ -31,6 +32,7 @@ export {
|
|
|
31
32
|
EXECUTOR_OWNERSHIP_PARAGRAPH,
|
|
32
33
|
EXECUTOR_LINK_PARAGRAPH,
|
|
33
34
|
EXECUTOR_RESULT_PARAGRAPH,
|
|
35
|
+
EXECUTOR_WORKING_PARAGRAPH,
|
|
34
36
|
createAndRecordDelegation,
|
|
35
37
|
isProvisionalThreadId,
|
|
36
38
|
normalizeCodexThreadId,
|
package/package.json
CHANGED
|
@@ -17,7 +17,8 @@ because it concerns TaskChef or a configured project. Explicit requests to
|
|
|
17
17
|
delegate separate work remain valid.
|
|
18
18
|
|
|
19
19
|
Use the bundled `prepare_dispatch`, `record_task`, `link_task`, and
|
|
20
|
-
`
|
|
20
|
+
`report_state` MCP tools directly. `report_result` is a deprecated compatibility
|
|
21
|
+
alias and must not be emitted into new executor instructions. Never fall back to shell writes. If a
|
|
21
22
|
required tool is unavailable, stop and report that the TaskChef plugin must be
|
|
22
23
|
reloaded or installed.
|
|
23
24
|
|
|
@@ -49,7 +50,9 @@ reloaded or installed.
|
|
|
49
50
|
|
|
50
51
|
> 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.
|
|
51
52
|
|
|
52
|
-
>
|
|
53
|
+
> After a successful initial link, and at the start of every follow-up turn before substantive work, read this exact Codex thread natively to obtain the current turn ID and call TaskChef report_state with the marked task ID, the self-linked thread ID, that current turn ID, status working, and summary omitted or null. link_task remains the first TaskChef action on the initial turn; do not report working before identity is linked. Never reuse a prior turn ID after a follow-up.
|
|
54
|
+
|
|
55
|
+
> Before ending, read this exact Codex thread again and call TaskChef report_state for the same current working turn with status completed, needs_input, or failed and a concise summary. 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.
|
|
53
56
|
|
|
54
57
|
5. Before creating each executor, call `record_task` exactly once with `id`,
|
|
55
58
|
`project`, `title`, the exact marked `instruction`, and `threadId: null`.
|
|
@@ -58,7 +61,7 @@ reloaded or installed.
|
|
|
58
61
|
7. Return immediately. Preserve a returned provisional client ID only for the
|
|
59
62
|
created-thread directive. Do not call `link_task` from the dispatcher even
|
|
60
63
|
when creation returns a durable ID; the child must self-link.
|
|
61
|
-
8. If creation fails after recording, call `
|
|
64
|
+
8. If creation fails after recording, call `report_state` with `failed`, null
|
|
62
65
|
thread/turn IDs, and a bounded summary before returning the failure.
|
|
63
66
|
|
|
64
67
|
## Executor contract
|
|
@@ -71,10 +74,12 @@ Identical retries are safe. A rejected link, unavailable tool, or interrupted
|
|
|
71
74
|
initial turn leaves the record visibly link-pending and retryable; the executor
|
|
72
75
|
must not guess or do substantive work first.
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
After linking on the initial turn, and before substantive work on every later
|
|
78
|
+
turn, the executor reads the exact thread and calls `report_state` with
|
|
79
|
+
`working`, the current turn ID, and no summary. Before ending that same turn it
|
|
80
|
+
reports a semantic state with the same turn ID and a summary. A follow-up must
|
|
81
|
+
use the new turn ID. `needs_input` is only for a real user decision, not live
|
|
82
|
+
approval UI.
|
|
78
83
|
|
|
79
|
-
The filesystem watcher surfaces `link_task` and `
|
|
84
|
+
The filesystem watcher surfaces `link_task` and `report_state` writes to the
|
|
80
85
|
dashboard. The linked child ID drives the exact Codex deep link.
|
|
@@ -44,37 +44,37 @@ all deterministic task-log operations.
|
|
|
44
44
|
detailed read. Native approval is live Codex state, not a `needs_input`
|
|
45
45
|
callback. An inactive status never proves semantic completion; it only
|
|
46
46
|
permits a trustworthy cached MCP result to stand.
|
|
47
|
-
4.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
read every idle terminal task in an overview merely because native
|
|
47
|
+
4. In schema 5, treat `status`, `turnId`, and `updatedAt` as the latest reported
|
|
48
|
+
execution state and treat `lastResult` as the separately preserved semantic
|
|
49
|
+
result. A `working` state with a non-null `lastResult` means a newer executor
|
|
50
|
+
turn started after that result; show the prior result as history, not as the
|
|
51
|
+
current outcome. Treat a failed `lastResult` with null thread and turn IDs as
|
|
52
|
+
a fresh executor-creation failure. No live read is possible or needed.
|
|
53
|
+
Schema 4 snapshots normalize a structurally complete result into
|
|
54
|
+
`lastResult` without rewriting their log line. When identity is certain and
|
|
55
|
+
metadata says the thread is inactive, trust the latest semantic result by
|
|
56
|
+
default in a broad overview unless a newer working state makes it historical.
|
|
57
|
+
Do not read every idle terminal task in an overview merely because native
|
|
58
58
|
`updatedAt` is later: callbacks normally run before Codex finalizes the same
|
|
59
59
|
turn, and overview performance matters more than investigating every rare
|
|
60
60
|
missed callback.
|
|
61
61
|
|
|
62
62
|
For a focused task, title, or project report, perform at most one detailed
|
|
63
63
|
read for each selected inactive task when matched metadata `updatedAt` is
|
|
64
|
-
later than
|
|
64
|
+
later than `lastResult.updatedAt`, by any amount. Read once as well
|
|
65
65
|
when there is no semantic callback, identity or metadata is uncertain or
|
|
66
66
|
contradictory, or the user explicitly requests a fully live result. If
|
|
67
67
|
focused metadata is not newer, trust the cache. Absence from the bounded
|
|
68
68
|
recent snapshot is not by itself a reason to read every cached terminal
|
|
69
69
|
overview entry. Batch immediate native reads with no more than eight targets
|
|
70
70
|
per call. When a detailed read occurs, compare the latest structured turn ID
|
|
71
|
-
and native turn state with
|
|
71
|
+
and native turn state with `lastResult.turnId`: a newer turn without a callback
|
|
72
72
|
makes the cache stale, while an interrupted or cancelled callback turn
|
|
73
73
|
cannot prove completion. Never classify assistant prose.
|
|
74
74
|
5. Report each task as one of: working, needs input, awaiting native approval,
|
|
75
75
|
completed, failed, unresolved, or unknown. Show the cached summary when it
|
|
76
76
|
remains fresh. If a newer turn exists without a callback, describe the live
|
|
77
|
-
state and label the
|
|
77
|
+
state and label the preserved result historical or stale rather than overwriting it.
|
|
78
78
|
6. Never edit `tasks.jsonl` directly during reporting. A null identity is
|
|
79
79
|
executor link-pending and must be retried by that executor. Never persist inferred status,
|
|
80
80
|
transcripts, prose classifications, or hidden reasoning. Do not poll or wait.
|
package/src/cli.js
CHANGED
|
@@ -166,18 +166,22 @@ function singleLineDetail(value) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function taskDetails(task) {
|
|
169
|
+
const lastResult = task.lastResult;
|
|
169
170
|
return [
|
|
170
171
|
`Title: ${singleLineDetail(task.title)}`,
|
|
171
172
|
`Project: ${singleLineDetail(task.project.name)}`,
|
|
172
|
-
`
|
|
173
|
-
`
|
|
173
|
+
`Current status: ${singleLineDetail(task.status ?? "unknown")}`,
|
|
174
|
+
`Current turn ID: ${singleLineDetail(task.turnId ?? "-")}`,
|
|
175
|
+
`Last result status: ${singleLineDetail(lastResult?.status ?? "-")}`,
|
|
176
|
+
`Last result summary: ${singleLineDetail(lastResult?.summary ?? "-")}`,
|
|
177
|
+
`Last result turn ID: ${singleLineDetail(lastResult?.turnId ?? "-")}`,
|
|
178
|
+
`Last result updated: ${singleLineDetail(lastResult?.updatedAt ?? "-")}`,
|
|
174
179
|
`Project path: ${singleLineDetail(task.project.path)}`,
|
|
175
180
|
`Created: ${singleLineDetail(task.createdAt)}`,
|
|
176
181
|
`Updated: ${singleLineDetail(task.updatedAt ?? "-")}`,
|
|
177
182
|
`Updated by: ${singleLineDetail(task.updatedBy ?? "-")}`,
|
|
178
183
|
`Task ID: ${singleLineDetail(task.id)}`,
|
|
179
184
|
`Thread ID: ${singleLineDetail(task.threadId ?? "-")}`,
|
|
180
|
-
`Turn ID: ${singleLineDetail(task.turnId ?? "-")}`,
|
|
181
185
|
"Instruction:",
|
|
182
186
|
task.instruction,
|
|
183
187
|
].join("\n");
|
package/src/dashboard/app.js
CHANGED
|
@@ -123,14 +123,18 @@ function openDialog(task) {
|
|
|
123
123
|
state.selectedTask = task;
|
|
124
124
|
elements.dialogProject.textContent = task.project.name;
|
|
125
125
|
elements.dialogTitle.textContent = task.title;
|
|
126
|
-
elements.dialogSummary.textContent = task.summary
|
|
126
|
+
elements.dialogSummary.textContent = task.lastResult?.summary
|
|
127
|
+
?? "No semantic result has been reported yet.";
|
|
127
128
|
elements.dialogInstruction.textContent = task.instruction;
|
|
128
129
|
elements.copyThreadId.disabled = !task.threadId;
|
|
129
130
|
elements.dialogMetadata.replaceChildren(
|
|
130
|
-
...detailRow("
|
|
131
|
+
...detailRow("Current status", taskStatusLabel(task)),
|
|
132
|
+
...detailRow("Current turn ID", task.turnId),
|
|
133
|
+
...detailRow("Last result status", task.lastResult?.status?.replaceAll("_", " ")),
|
|
134
|
+
...detailRow("Last result turn ID", task.lastResult?.turnId),
|
|
135
|
+
...detailRow("Last result updated", formatTime(task.lastResult?.updatedAt)),
|
|
131
136
|
...detailRow("Task ID", task.id),
|
|
132
137
|
...detailRow("Thread ID", task.threadId),
|
|
133
|
-
...detailRow("Turn ID", task.turnId),
|
|
134
138
|
...detailRow("Project path", task.project.path),
|
|
135
139
|
...detailRow("Created", formatTime(task.createdAt)),
|
|
136
140
|
...detailRow("Updated", formatTime(
|
|
@@ -160,7 +164,7 @@ function taskCard(task) {
|
|
|
160
164
|
project.textContent = task.project.name;
|
|
161
165
|
const summary = document.createElement("p");
|
|
162
166
|
summary.className = "task-summary";
|
|
163
|
-
summary.textContent = task.summary ?? "No semantic result reported yet.";
|
|
167
|
+
summary.textContent = task.lastResult?.summary ?? "No semantic result reported yet.";
|
|
164
168
|
const time = document.createElement("time");
|
|
165
169
|
time.dateTime = task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt;
|
|
166
170
|
time.textContent = `Updated ${formatTime(time.dateTime)}`;
|
package/src/dashboard/state.js
CHANGED
package/src/dashboard.js
CHANGED
|
@@ -131,6 +131,8 @@ function assertDashboardTaskBounds(tasks, maximumTasks) {
|
|
|
131
131
|
boundedText(task.summary, 2_000, `${name} summary`);
|
|
132
132
|
boundedText(task.threadId, 512, `${name} thread ID`);
|
|
133
133
|
boundedText(task.turnId, 512, `${name} turn ID`);
|
|
134
|
+
boundedText(task.lastResult?.summary, 2_000, `${name} last result summary`);
|
|
135
|
+
boundedText(task.lastResult?.turnId, 512, `${name} last result turn ID`);
|
|
134
136
|
boundedText(task.project.name, 1_000, `${name} project name`);
|
|
135
137
|
boundedText(task.project.path, 8_192, `${name} project path`);
|
|
136
138
|
boundedText(task.project.description, 4_000, `${name} project description`);
|
package/src/delegation.js
CHANGED
|
@@ -2,7 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
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.";
|
|
4
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
|
|
5
|
+
export const EXECUTOR_WORKING_PARAGRAPH = "After a successful initial link, and at the start of every follow-up turn before substantive work, read this exact Codex thread natively to obtain the current turn ID and call TaskChef report_state with the marked task ID, the self-linked thread ID, that current turn ID, status working, and summary omitted or null. link_task remains the first TaskChef action on the initial turn; do not report working before identity is linked. Never reuse a prior turn ID after a follow-up.";
|
|
6
|
+
export const EXECUTOR_RESULT_PARAGRAPH = "Before ending, read this exact Codex thread again and call TaskChef report_state for the same current working turn with status completed, needs_input, or failed and a concise summary. 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.";
|
|
6
7
|
|
|
7
8
|
const UUID_SOURCE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
8
9
|
const UUID_PATTERN = new RegExp(`^${UUID_SOURCE}$`);
|
|
@@ -96,7 +97,7 @@ export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
|
96
97
|
const id = requireUuid(taskId);
|
|
97
98
|
return {
|
|
98
99
|
id,
|
|
99
|
-
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${EXECUTOR_LINK_PARAGRAPH}\n\n${EXECUTOR_RESULT_PARAGRAPH}\n\n${instruction}`,
|
|
100
|
+
instruction: `${taskChefMarker(id)}\n\n${EXECUTOR_OWNERSHIP_PARAGRAPH}\n\n${EXECUTOR_LINK_PARAGRAPH}\n\n${EXECUTOR_WORKING_PARAGRAPH}\n\n${EXECUTOR_RESULT_PARAGRAPH}\n\n${instruction}`,
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
|
package/src/mcp.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
prepareDispatch,
|
|
5
5
|
linkTask,
|
|
6
6
|
recordTask,
|
|
7
|
+
reportTaskState,
|
|
7
8
|
reportTaskResult,
|
|
8
9
|
} from "./workspace.js";
|
|
9
10
|
import { parseTaskChefMarker } from "./delegation.js";
|
|
@@ -18,7 +19,7 @@ const projectSchema = z.object({
|
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
const taskSchema = z.object({
|
|
21
|
-
schemaVersion: z.literal(4),
|
|
22
|
+
schemaVersion: z.union([z.literal(4), z.literal(5)]),
|
|
22
23
|
id: z.string(),
|
|
23
24
|
project: projectSchema,
|
|
24
25
|
title: z.string(),
|
|
@@ -30,6 +31,12 @@ const taskSchema = z.object({
|
|
|
30
31
|
turnId: z.string().nullable(),
|
|
31
32
|
updatedAt: z.string(),
|
|
32
33
|
updatedBy: z.enum(["dispatcher", "mcp"]),
|
|
34
|
+
lastResult: z.object({
|
|
35
|
+
status: z.enum(["needs_input", "completed", "failed"]),
|
|
36
|
+
summary: z.string(),
|
|
37
|
+
turnId: z.string().nullable(),
|
|
38
|
+
updatedAt: z.string(),
|
|
39
|
+
}).nullable(),
|
|
33
40
|
});
|
|
34
41
|
|
|
35
42
|
const preparationSchema = z.object({
|
|
@@ -54,13 +61,14 @@ export function createTaskChefMcpServer({
|
|
|
54
61
|
prepare = prepareDispatch,
|
|
55
62
|
record = recordTask,
|
|
56
63
|
reportResult = reportTaskResult,
|
|
64
|
+
reportState = reportTaskState,
|
|
57
65
|
link = linkTask,
|
|
58
66
|
} = {}) {
|
|
59
67
|
const server = new McpServer(
|
|
60
68
|
{ name: "taskchef", version: "1.0.0" },
|
|
61
69
|
{
|
|
62
70
|
instructions:
|
|
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
|
|
71
|
+
"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 first, report_state working at the start of each execution turn, and report_state with a semantic outcome before ending.",
|
|
64
72
|
},
|
|
65
73
|
);
|
|
66
74
|
|
|
@@ -136,12 +144,38 @@ export function createTaskChefMcpServer({
|
|
|
136
144
|
},
|
|
137
145
|
);
|
|
138
146
|
|
|
147
|
+
server.registerTool(
|
|
148
|
+
"report_state",
|
|
149
|
+
{
|
|
150
|
+
title: "Report TaskChef state",
|
|
151
|
+
description:
|
|
152
|
+
"Report this self-linked executor turn's lifecycle state. Use working before substantive work in a newly linked or follow-up turn, with summary omitted or null. Before ending the same turn, report needs_input, completed, or failed with a concise semantic summary. Exact retries are idempotent; stale or mismatched turns are rejected.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
taskId: z.string().min(1),
|
|
155
|
+
threadId: z.string().min(1).nullable(),
|
|
156
|
+
turnId: z.string().min(1).max(256).nullable(),
|
|
157
|
+
status: z.enum(["working", "needs_input", "completed", "failed"]),
|
|
158
|
+
summary: z.string().min(1).max(2_000).nullable().optional(),
|
|
159
|
+
},
|
|
160
|
+
outputSchema: { task: taskSchema },
|
|
161
|
+
annotations: {
|
|
162
|
+
readOnlyHint: false,
|
|
163
|
+
destructiveHint: true,
|
|
164
|
+
openWorldHint: false,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
async (input) => {
|
|
168
|
+
const task = await reportState(workspace, input);
|
|
169
|
+
return toolResult("task", task, `Recorded ${task.status} state for TaskChef task ${task.id}.`);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
139
173
|
server.registerTool(
|
|
140
174
|
"report_result",
|
|
141
175
|
{
|
|
142
|
-
title: "Report TaskChef result",
|
|
176
|
+
title: "Report TaskChef result (deprecated)",
|
|
143
177
|
description:
|
|
144
|
-
"
|
|
178
|
+
"Deprecated compatibility alias for semantic results. New executors must use report_state working at turn start and report_state again with needs_input, completed, or failed before ending. This alias preserves legacy callers by implicitly starting the supplied newer turn before storing its result.",
|
|
145
179
|
inputSchema: {
|
|
146
180
|
taskId: z.string().min(1),
|
|
147
181
|
threadId: z.string().min(1).nullable(),
|
package/src/workspace.js
CHANGED
|
@@ -38,7 +38,8 @@ const DISPATCH_FILE_NAME = "tasks.jsonl";
|
|
|
38
38
|
const WORKSPACE_LOCK_NAME = ".taskchef-workspace.lock";
|
|
39
39
|
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
40
40
|
const CURRENT_CONFIG_SCHEMA_VERSION = 2;
|
|
41
|
-
const CURRENT_TASK_SCHEMA_VERSION =
|
|
41
|
+
const CURRENT_TASK_SCHEMA_VERSION = 5;
|
|
42
|
+
const PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION = 4;
|
|
42
43
|
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
43
44
|
const PROJECT_FIELDS = new Set([
|
|
44
45
|
"name",
|
|
@@ -48,7 +49,7 @@ const PROJECT_FIELDS = new Set([
|
|
|
48
49
|
"description",
|
|
49
50
|
]);
|
|
50
51
|
const PROJECT_INPUT_FIELDS = new Set(["name", "path", "githubRepos", "description"]);
|
|
51
|
-
const
|
|
52
|
+
const STATEFUL_DISPATCH_FIELDS = new Set([
|
|
52
53
|
"schemaVersion",
|
|
53
54
|
"id",
|
|
54
55
|
"project",
|
|
@@ -62,6 +63,7 @@ const DISPATCH_FIELDS = new Set([
|
|
|
62
63
|
"updatedAt",
|
|
63
64
|
"updatedBy",
|
|
64
65
|
]);
|
|
66
|
+
const DISPATCH_FIELDS = new Set([...STATEFUL_DISPATCH_FIELDS, "lastResult"]);
|
|
65
67
|
const RECORD_DISPATCH_FIELDS = new Set([
|
|
66
68
|
"id",
|
|
67
69
|
"project",
|
|
@@ -73,6 +75,7 @@ const RESULT_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
|
73
75
|
const TASK_STATUSES = new Set(["working", ...RESULT_STATUSES]);
|
|
74
76
|
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "mcp"]);
|
|
75
77
|
const MAX_RESULT_SUMMARY_LENGTH = 2_000;
|
|
78
|
+
const LAST_RESULT_FIELDS = new Set(["status", "summary", "turnId", "updatedAt"]);
|
|
76
79
|
|
|
77
80
|
function requireExactFields(value, fields, name) {
|
|
78
81
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -100,6 +103,13 @@ function requireTimestamp(value, name) {
|
|
|
100
103
|
return value;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function transitionTimestamp(now, currentUpdatedAt) {
|
|
107
|
+
const candidate = requireTimestamp(now ?? new Date().toISOString(), "transition timestamp");
|
|
108
|
+
return Date.parse(candidate) < Date.parse(currentUpdatedAt)
|
|
109
|
+
? currentUpdatedAt
|
|
110
|
+
: candidate;
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
function optionalString(value, name, { maxLength = null } = {}) {
|
|
104
114
|
if (value === null) return null;
|
|
105
115
|
const normalized = requireString(value, name).trim();
|
|
@@ -668,14 +678,62 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
668
678
|
}
|
|
669
679
|
|
|
670
680
|
async function validateDispatchShape(dispatch, name = "task") {
|
|
671
|
-
|
|
681
|
+
const supportedVersions = [
|
|
682
|
+
PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION,
|
|
683
|
+
CURRENT_TASK_SCHEMA_VERSION,
|
|
684
|
+
];
|
|
685
|
+
if (!supportedVersions.includes(dispatch?.schemaVersion)) {
|
|
672
686
|
throw new Error(`unsupported ${name} schemaVersion`);
|
|
673
687
|
}
|
|
674
|
-
requireExactFields(
|
|
688
|
+
requireExactFields(
|
|
689
|
+
dispatch,
|
|
690
|
+
dispatch.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION
|
|
691
|
+
? DISPATCH_FIELDS
|
|
692
|
+
: STATEFUL_DISPATCH_FIELDS,
|
|
693
|
+
name,
|
|
694
|
+
);
|
|
675
695
|
const id = requireSafeId(dispatch.id, `${name}.id`);
|
|
676
696
|
const project = await normalizeProject(dispatch.project, 0, { checkPath: false });
|
|
697
|
+
const status = requireEnum(dispatch.status, TASK_STATUSES, `${name}.status`);
|
|
698
|
+
const summary = optionalString(dispatch.summary, `${name}.summary`, {
|
|
699
|
+
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
700
|
+
});
|
|
701
|
+
const turnId = optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 });
|
|
702
|
+
const updatedAt = requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`);
|
|
703
|
+
let lastResult = null;
|
|
704
|
+
if (dispatch.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION) {
|
|
705
|
+
if (dispatch.lastResult !== null) {
|
|
706
|
+
requireExactFields(dispatch.lastResult, LAST_RESULT_FIELDS, `${name}.lastResult`);
|
|
707
|
+
lastResult = {
|
|
708
|
+
status: requireEnum(
|
|
709
|
+
dispatch.lastResult.status,
|
|
710
|
+
RESULT_STATUSES,
|
|
711
|
+
`${name}.lastResult.status`,
|
|
712
|
+
),
|
|
713
|
+
summary: optionalString(
|
|
714
|
+
dispatch.lastResult.summary,
|
|
715
|
+
`${name}.lastResult.summary`,
|
|
716
|
+
{ maxLength: MAX_RESULT_SUMMARY_LENGTH },
|
|
717
|
+
),
|
|
718
|
+
turnId: optionalString(
|
|
719
|
+
dispatch.lastResult.turnId,
|
|
720
|
+
`${name}.lastResult.turnId`,
|
|
721
|
+
{ maxLength: 256 },
|
|
722
|
+
),
|
|
723
|
+
updatedAt: requireTimestamp(
|
|
724
|
+
dispatch.lastResult.updatedAt,
|
|
725
|
+
`${name}.lastResult.updatedAt`,
|
|
726
|
+
),
|
|
727
|
+
};
|
|
728
|
+
if (lastResult.summary === null) {
|
|
729
|
+
throw new Error(`${name}.lastResult.summary must be a non-empty string`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} else if (RESULT_STATUSES.has(status)) {
|
|
733
|
+
lastResult = { status, summary, turnId, updatedAt };
|
|
734
|
+
}
|
|
677
735
|
const normalized = {
|
|
678
|
-
schemaVersion:
|
|
736
|
+
schemaVersion: dispatch.schemaVersion,
|
|
679
737
|
id,
|
|
680
738
|
project,
|
|
681
739
|
title: requireString(dispatch.title, `${name}.title`).trim(),
|
|
@@ -684,20 +742,93 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
684
742
|
? null
|
|
685
743
|
: normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),
|
|
686
744
|
createdAt: requireTimestamp(dispatch.createdAt, `${name}.createdAt`),
|
|
687
|
-
status
|
|
688
|
-
summary
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
turnId: optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 }),
|
|
692
|
-
updatedAt: requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`),
|
|
745
|
+
status,
|
|
746
|
+
summary,
|
|
747
|
+
turnId,
|
|
748
|
+
updatedAt,
|
|
693
749
|
updatedBy: requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`),
|
|
750
|
+
lastResult,
|
|
694
751
|
};
|
|
752
|
+
const isSelfLinkingRecord = normalized.threadId !== null
|
|
753
|
+
&& parseTaskChefMarker(normalized.instruction) === normalized.id;
|
|
754
|
+
if (isSelfLinkingRecord) {
|
|
755
|
+
if (normalized.turnId !== null) {
|
|
756
|
+
normalizeCodexThreadId(normalized.turnId, `${name}.turnId`);
|
|
757
|
+
}
|
|
758
|
+
if (normalized.lastResult?.turnId != null) {
|
|
759
|
+
normalizeCodexThreadId(
|
|
760
|
+
normalized.lastResult.turnId,
|
|
761
|
+
`${name}.lastResult.turnId`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (normalized.schemaVersion >= PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION) {
|
|
766
|
+
if (normalized.threadId === null) {
|
|
767
|
+
const isLinkPending = normalized.status === "working"
|
|
768
|
+
&& normalized.summary === null
|
|
769
|
+
&& normalized.turnId === null
|
|
770
|
+
&& normalized.lastResult === null
|
|
771
|
+
&& normalized.updatedBy === "dispatcher";
|
|
772
|
+
const isCreationFailure = normalized.status === "failed"
|
|
773
|
+
&& normalized.summary !== null
|
|
774
|
+
&& normalized.turnId === null
|
|
775
|
+
&& normalized.lastResult?.status === "failed"
|
|
776
|
+
&& normalized.lastResult.turnId === null
|
|
777
|
+
&& normalized.updatedBy === "mcp";
|
|
778
|
+
if (!isLinkPending && !isCreationFailure) {
|
|
779
|
+
throw new Error(`${name} has an invalid unlinked lifecycle state`);
|
|
780
|
+
}
|
|
781
|
+
} else {
|
|
782
|
+
if (RESULT_STATUSES.has(normalized.status) && normalized.turnId === null) {
|
|
783
|
+
throw new Error(`${name}.turnId is required for a linked semantic state`);
|
|
784
|
+
}
|
|
785
|
+
if (normalized.lastResult !== null && normalized.lastResult.turnId === null) {
|
|
786
|
+
throw new Error(`${name}.lastResult.turnId is required for a linked result`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
695
790
|
if (normalized.status === "working" && normalized.summary !== null) {
|
|
696
791
|
throw new Error(`${name}.summary must be null while status is working`);
|
|
697
792
|
}
|
|
698
793
|
if (RESULT_STATUSES.has(normalized.status) && normalized.summary === null) {
|
|
699
794
|
throw new Error(`${name}.summary is required for status ${normalized.status}`);
|
|
700
795
|
}
|
|
796
|
+
if (normalized.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION) {
|
|
797
|
+
if (RESULT_STATUSES.has(normalized.status)) {
|
|
798
|
+
if (
|
|
799
|
+
normalized.lastResult === null
|
|
800
|
+
|| normalized.lastResult.status !== normalized.status
|
|
801
|
+
|| normalized.lastResult.summary !== normalized.summary
|
|
802
|
+
|| normalized.lastResult.turnId !== normalized.turnId
|
|
803
|
+
|| normalized.lastResult.updatedAt !== normalized.updatedAt
|
|
804
|
+
) {
|
|
805
|
+
throw new Error(`${name}.lastResult must match the current semantic state`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (
|
|
809
|
+
normalized.lastResult !== null
|
|
810
|
+
&& Date.parse(normalized.lastResult.updatedAt) < Date.parse(normalized.createdAt)
|
|
811
|
+
) {
|
|
812
|
+
throw new Error(`${name}.lastResult.updatedAt must not be earlier than createdAt`);
|
|
813
|
+
}
|
|
814
|
+
if (
|
|
815
|
+
normalized.lastResult !== null
|
|
816
|
+
&& Date.parse(normalized.lastResult.updatedAt) > Date.parse(normalized.updatedAt)
|
|
817
|
+
) {
|
|
818
|
+
throw new Error(`${name}.lastResult.updatedAt must not be later than updatedAt`);
|
|
819
|
+
}
|
|
820
|
+
if (
|
|
821
|
+
normalized.status === "working"
|
|
822
|
+
&& isSelfLinkingRecord
|
|
823
|
+
&& normalized.lastResult?.turnId != null
|
|
824
|
+
&& (
|
|
825
|
+
normalized.turnId === null
|
|
826
|
+
|| normalized.turnId <= normalized.lastResult.turnId
|
|
827
|
+
)
|
|
828
|
+
) {
|
|
829
|
+
throw new Error(`${name}.turnId must be newer than lastResult.turnId while working`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
701
832
|
if (
|
|
702
833
|
Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
703
834
|
) {
|
|
@@ -802,6 +933,7 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
802
933
|
turnId: null,
|
|
803
934
|
updatedAt: createdAt,
|
|
804
935
|
updatedBy: "dispatcher",
|
|
936
|
+
lastResult: null,
|
|
805
937
|
});
|
|
806
938
|
const existing = await readDispatchesUnlocked(root);
|
|
807
939
|
if (existing.some((item) => item.id === dispatch.id)) {
|
|
@@ -845,8 +977,9 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
845
977
|
if (dispatch.threadId === durableThreadId) return dispatch;
|
|
846
978
|
const canonical = await validateDispatchShape({
|
|
847
979
|
...dispatch,
|
|
980
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
848
981
|
threadId: durableThreadId,
|
|
849
|
-
updatedAt: now
|
|
982
|
+
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
850
983
|
updatedBy: "mcp",
|
|
851
984
|
});
|
|
852
985
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
@@ -875,8 +1008,9 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
875
1008
|
}
|
|
876
1009
|
const linked = await validateDispatchShape({
|
|
877
1010
|
...dispatch,
|
|
1011
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
878
1012
|
threadId: durableThreadId,
|
|
879
|
-
updatedAt: now
|
|
1013
|
+
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
880
1014
|
updatedBy: "mcp",
|
|
881
1015
|
});
|
|
882
1016
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
@@ -895,22 +1029,53 @@ function dispatchLineWithState(dispatch, patch) {
|
|
|
895
1029
|
});
|
|
896
1030
|
}
|
|
897
1031
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
new
|
|
902
|
-
|
|
903
|
-
);
|
|
1032
|
+
function normalizeTaskStateInput(input, { allowWorking }) {
|
|
1033
|
+
const fields = new Set(["taskId", "threadId", "turnId", "status", "summary"]);
|
|
1034
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1035
|
+
throw new Error("task state must be an object");
|
|
1036
|
+
}
|
|
1037
|
+
const unexpected = Object.keys(input).find((key) => !fields.has(key));
|
|
1038
|
+
if (unexpected) throw new Error(`task state has unsupported field: ${unexpected}`);
|
|
1039
|
+
for (const field of ["taskId", "threadId", "turnId", "status"]) {
|
|
1040
|
+
if (!(field in input)) throw new Error(`task state is missing field: ${field}`);
|
|
1041
|
+
}
|
|
904
1042
|
const id = requireSafeId(input.taskId, "taskId");
|
|
905
1043
|
const threadId = input.threadId === null
|
|
906
1044
|
? null
|
|
907
1045
|
: normalizeDurableThreadId(input.threadId, "threadId");
|
|
908
1046
|
const turnId = optionalString(input.turnId, "turnId", { maxLength: 256 });
|
|
909
|
-
const status = requireEnum(
|
|
910
|
-
|
|
1047
|
+
const status = requireEnum(
|
|
1048
|
+
input.status,
|
|
1049
|
+
allowWorking ? TASK_STATUSES : RESULT_STATUSES,
|
|
1050
|
+
"status",
|
|
1051
|
+
);
|
|
1052
|
+
const summary = optionalString("summary" in input ? input.summary : null, "summary", {
|
|
911
1053
|
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
912
1054
|
});
|
|
913
|
-
if (
|
|
1055
|
+
if (status === "working" && summary !== null) {
|
|
1056
|
+
throw new Error("summary must be null while status is working");
|
|
1057
|
+
}
|
|
1058
|
+
if (status !== "working" && summary === null) {
|
|
1059
|
+
throw new Error(`summary is required for status ${status}`);
|
|
1060
|
+
}
|
|
1061
|
+
return { id, threadId, turnId, status, summary };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function sameLastResult(lastResult, { status, summary, turnId }) {
|
|
1065
|
+
return lastResult !== null
|
|
1066
|
+
&& lastResult.status === status
|
|
1067
|
+
&& lastResult.summary === summary
|
|
1068
|
+
&& lastResult.turnId === turnId;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
async function reportTaskStateInternal(
|
|
1072
|
+
workspaceRoot,
|
|
1073
|
+
input,
|
|
1074
|
+
{ now, compatibilityAlias = false } = {},
|
|
1075
|
+
) {
|
|
1076
|
+
const { id, threadId, turnId, status, summary } = normalizeTaskStateInput(input, {
|
|
1077
|
+
allowWorking: !compatibilityAlias,
|
|
1078
|
+
});
|
|
914
1079
|
const root = await realpath(path.resolve(workspaceRoot));
|
|
915
1080
|
return withWorkspaceLock(root, async () => {
|
|
916
1081
|
const records = await readDispatchRecordsUnlocked(root);
|
|
@@ -922,45 +1087,120 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
922
1087
|
dispatch.threadId !== null
|
|
923
1088
|
&& parseTaskChefMarker(dispatch.instruction) === dispatch.id
|
|
924
1089
|
);
|
|
925
|
-
|
|
1090
|
+
if (
|
|
1091
|
+
!compatibilityAlias
|
|
1092
|
+
&& dispatch.threadId !== null
|
|
1093
|
+
&& !isSelfLinkingJourney
|
|
1094
|
+
) {
|
|
1095
|
+
throw new Error(`report_state accepts only self-linked task records: ${id}`);
|
|
1096
|
+
}
|
|
1097
|
+
let stateTurnId = turnId;
|
|
926
1098
|
if (isSelfLinkingJourney) {
|
|
927
1099
|
if (dispatch.updatedBy === "dispatcher") {
|
|
928
1100
|
throw new Error(`self-linking task must link before reporting a result: ${id}`);
|
|
929
1101
|
}
|
|
930
|
-
|
|
1102
|
+
stateTurnId = normalizeCodexThreadId(turnId, "turnId");
|
|
931
1103
|
}
|
|
932
1104
|
if (dispatch.threadId === null) {
|
|
933
|
-
if (threadId !== null ||
|
|
1105
|
+
if (threadId !== null || stateTurnId !== null || status !== "failed") {
|
|
934
1106
|
throw new Error(`task without a durable threadId accepts only failed with null thread/turn IDs: ${id}`);
|
|
935
1107
|
}
|
|
1108
|
+
if (!compatibilityAlias) {
|
|
1109
|
+
const rawSchemaVersion = records[index].raw.schemaVersion;
|
|
1110
|
+
const hasCurrentMarker = rawSchemaVersion
|
|
1111
|
+
>= PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION
|
|
1112
|
+
&& parseTaskChefMarker(dispatch.instruction) === dispatch.id;
|
|
1113
|
+
const isFreshCreationFailure = hasCurrentMarker
|
|
1114
|
+
&& dispatch.status === "working"
|
|
1115
|
+
&& dispatch.turnId === null
|
|
1116
|
+
&& dispatch.lastResult === null
|
|
1117
|
+
&& dispatch.updatedBy === "dispatcher";
|
|
1118
|
+
const isIdenticalCreationFailureRetry = hasCurrentMarker
|
|
1119
|
+
&& dispatch.status === "failed"
|
|
1120
|
+
&& dispatch.turnId === null
|
|
1121
|
+
&& dispatch.updatedBy === "mcp"
|
|
1122
|
+
&& sameLastResult(dispatch.lastResult, {
|
|
1123
|
+
status,
|
|
1124
|
+
summary,
|
|
1125
|
+
turnId: stateTurnId,
|
|
1126
|
+
});
|
|
1127
|
+
if (!isFreshCreationFailure && !isIdenticalCreationFailureRetry) {
|
|
1128
|
+
throw new Error(`report_state unlinked failure requires a fresh link-pending task: ${id}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
936
1131
|
} else {
|
|
937
1132
|
if (threadIdentityKey(threadId) !== threadIdentityKey(dispatch.threadId)) {
|
|
938
1133
|
throw new Error(`task result threadId does not match recorded threadId: ${id}`);
|
|
939
1134
|
}
|
|
940
|
-
if (
|
|
941
|
-
throw new Error(`task
|
|
1135
|
+
if (stateTurnId === null) {
|
|
1136
|
+
throw new Error(`task state turnId is required for a linked task: ${id}`);
|
|
942
1137
|
}
|
|
943
1138
|
}
|
|
944
|
-
if (
|
|
945
|
-
if (status ===
|
|
946
|
-
|
|
1139
|
+
if (status === "working") {
|
|
1140
|
+
if (dispatch.status === "working" && stateTurnId === dispatch.turnId) return dispatch;
|
|
1141
|
+
if (isSelfLinkingJourney) {
|
|
1142
|
+
if (dispatch.turnId !== null && stateTurnId <= dispatch.turnId) {
|
|
1143
|
+
throw new Error(`working turnId must be newer than the current task turnId: ${id}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (dispatch.lastResult?.turnId != null && stateTurnId <= dispatch.lastResult.turnId) {
|
|
1146
|
+
throw new Error(`working turnId must be newer than the last result turnId: ${id}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const updatedAt = transitionTimestamp(now, dispatch.updatedAt);
|
|
1150
|
+
const updated = await validateDispatchShape({
|
|
1151
|
+
...dispatch,
|
|
1152
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1153
|
+
status,
|
|
1154
|
+
summary: null,
|
|
1155
|
+
turnId: stateTurnId,
|
|
1156
|
+
updatedAt,
|
|
1157
|
+
updatedBy: "mcp",
|
|
1158
|
+
lastResult: dispatch.lastResult,
|
|
1159
|
+
});
|
|
1160
|
+
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1161
|
+
? dispatchLineWithState(updated, {})
|
|
1162
|
+
: record.line);
|
|
1163
|
+
await writeDispatchLinesAtomic(root, lines);
|
|
1164
|
+
return updated;
|
|
947
1165
|
}
|
|
948
1166
|
if (
|
|
949
|
-
|
|
950
|
-
&& dispatch.turnId
|
|
951
|
-
&&
|
|
1167
|
+
dispatch.status === status
|
|
1168
|
+
&& dispatch.turnId === stateTurnId
|
|
1169
|
+
&& dispatch.summary === summary
|
|
1170
|
+
&& sameLastResult(dispatch.lastResult, { status, summary, turnId: stateTurnId })
|
|
952
1171
|
) {
|
|
953
|
-
|
|
1172
|
+
return dispatch;
|
|
1173
|
+
}
|
|
1174
|
+
if (dispatch.turnId === stateTurnId && dispatch.status !== "working") {
|
|
1175
|
+
throw new Error(`task turn already has a different semantic result: ${id}`);
|
|
954
1176
|
}
|
|
955
|
-
|
|
1177
|
+
if (compatibilityAlias) {
|
|
1178
|
+
const matchesWorkingTurn = dispatch.status === "working"
|
|
1179
|
+
&& dispatch.turnId === stateTurnId;
|
|
1180
|
+
if (!matchesWorkingTurn && isSelfLinkingJourney) {
|
|
1181
|
+
if (dispatch.turnId !== null && stateTurnId <= dispatch.turnId) {
|
|
1182
|
+
throw new Error(`task result turnId must be newer than the stored turnId: ${id}`);
|
|
1183
|
+
}
|
|
1184
|
+
if (dispatch.lastResult?.turnId != null && stateTurnId <= dispatch.lastResult.turnId) {
|
|
1185
|
+
throw new Error(`task result turnId must be newer than the last result turnId: ${id}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} else if (dispatch.status !== "working" || dispatch.turnId !== stateTurnId) {
|
|
1189
|
+
throw new Error(`task result must match the current working turnId: ${id}`);
|
|
1190
|
+
}
|
|
1191
|
+
const updatedAt = transitionTimestamp(now, dispatch.updatedAt);
|
|
1192
|
+
const lastResult = { status, summary, turnId: stateTurnId, updatedAt };
|
|
1193
|
+
const candidate = {
|
|
956
1194
|
...dispatch,
|
|
957
1195
|
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
958
1196
|
status,
|
|
959
1197
|
summary,
|
|
960
|
-
turnId:
|
|
961
|
-
updatedAt
|
|
1198
|
+
turnId: stateTurnId,
|
|
1199
|
+
updatedAt,
|
|
962
1200
|
updatedBy: "mcp",
|
|
963
|
-
|
|
1201
|
+
lastResult,
|
|
1202
|
+
};
|
|
1203
|
+
const updated = await validateDispatchShape(candidate);
|
|
964
1204
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
965
1205
|
? dispatchLineWithState(updated, {})
|
|
966
1206
|
: record.line);
|
|
@@ -969,6 +1209,17 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
969
1209
|
});
|
|
970
1210
|
}
|
|
971
1211
|
|
|
1212
|
+
export async function reportTaskState(workspaceRoot, input, { now } = {}) {
|
|
1213
|
+
return reportTaskStateInternal(workspaceRoot, input, { now });
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
export async function reportTaskResult(workspaceRoot, input, options = {}) {
|
|
1217
|
+
return reportTaskStateInternal(workspaceRoot, input, {
|
|
1218
|
+
...options,
|
|
1219
|
+
compatibilityAlias: true,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
972
1223
|
export async function readTask(workspaceRoot, taskId) {
|
|
973
1224
|
const id = requireSafeId(taskId, "taskId");
|
|
974
1225
|
const dispatch = (await listTasks(workspaceRoot)).find((item) => item.id === id);
|